killbill-memoizeit
Changes
.gitignore 4(+4 -0)
.idea/codeStyleSettings.xml 200(+0 -200)
.idea/libraries/Maven__com_fasterxml_jackson_dataformat_jackson_dataformat_smile_2_0_1.xml 13(+0 -13)
.idea/libraries/Maven__com_fasterxml_jackson_module_jackson_module_jaxb_annotations_2_0_0.xml 13(+0 -13)
.idea/libraries/Maven__org_apache_felix_org_apache_felix_dependencymanager_annotation_3_1_0.xml 13(+0 -13)
.idea/libraries/Maven__org_apache_felix_org_apache_felix_dependencymanager_compat_3_0_1.xml 13(+0 -13)
.idea/libraries/Maven__org_apache_felix_org_apache_felix_dependencymanager_runtime_3_1_0.xml 13(+0 -13)
.idea/libraries/Maven__org_apache_felix_org_apache_felix_dependencymanager_shell_3_0_1.xml 13(+0 -13)
.idea/libraries/Maven__org_codehaus_plexus_plexus_container_default_1_0_alpha_9_stable_1.xml 13(+0 -13)
.idea/modules.xml 1(+0 -1)
.idea/vcs.xml 2(+1 -1)
account/pom.xml 70(+25 -45)
account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java 32(+16 -16)
account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountChangeEvent.java 12(+6 -6)
account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountCreationEvent.java 12(+6 -6)
account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApi.java 29(+12 -17)
account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApiWithMocks.java 30(+15 -15)
account/src/test/java/org/killbill/billing/account/glue/TestAccountModuleWithEmbeddedDB.java 10(+5 -5)
api/pom.xml 28(+14 -14)
api/src/main/java/com/ning/billing/overdue/applicator/formatters/BillingStateFormatter.java 30(+0 -30)
api/src/main/java/com/ning/billing/overdue/applicator/formatters/OverdueEmailFormatterFactory.java 24(+0 -24)
api/src/main/java/com/ning/billing/subscription/api/migration/SubscriptionBaseMigrationApi.java 133(+0 -133)
api/src/main/java/com/ning/billing/subscription/api/migration/SubscriptionBaseMigrationApiException.java 38(+0 -38)
api/src/main/java/com/ning/billing/subscription/api/timeline/SubscriptionBaseRepairException.java 43(+0 -43)
api/src/main/java/com/ning/billing/subscription/api/timeline/SubscriptionBaseTimeline.java 96(+0 -96)
api/src/main/java/com/ning/billing/subscription/api/timeline/SubscriptionBaseTimelineApi.java 38(+0 -38)
api/src/main/java/com/ning/billing/subscription/api/transfer/SubscriptionBaseTransferApi.java 46(+0 -46)
api/src/main/java/com/ning/billing/subscription/api/transfer/SubscriptionBaseTransferApiException.java 47(+0 -47)
api/src/main/java/com/ning/billing/subscription/api/user/SubscriptionBaseApiException.java 42(+0 -42)
api/src/main/java/org/killbill/billing/events/ControlTagDefinitionCreationInternalEvent.java 2(+1 -1)
api/src/main/java/org/killbill/billing/events/ControlTagDefinitionDeletionInternalEvent.java 2(+1 -1)
api/src/main/java/org/killbill/billing/events/UserTagDefinitionCreationInternalEvent.java 21(+21 -0)
api/src/main/java/org/killbill/billing/events/UserTagDefinitionDeletionInternalEvent.java 21(+21 -0)
api/src/main/java/org/killbill/billing/invoice/api/formatters/InvoiceFormatterFactory.java 27(+27 -0)
api/src/main/java/org/killbill/billing/overdue/applicator/formatters/BillingStateFormatter.java 30(+30 -0)
api/src/main/java/org/killbill/billing/overdue/applicator/formatters/OverdueEmailFormatterFactory.java 24(+24 -0)
api/src/main/java/org/killbill/billing/subscription/api/migration/SubscriptionBaseMigrationApi.java 133(+133 -0)
api/src/main/java/org/killbill/billing/subscription/api/migration/SubscriptionBaseMigrationApiException.java 38(+38 -0)
api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java 67(+67 -0)
api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBillingApiException.java 36(+36 -0)
api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseRepairException.java 43(+43 -0)
api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java 96(+96 -0)
api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimelineApi.java 38(+38 -0)
api/src/main/java/org/killbill/billing/subscription/api/transfer/SubscriptionBaseTransferApi.java 46(+46 -0)
api/src/main/java/org/killbill/billing/subscription/api/transfer/SubscriptionBaseTransferApiException.java 47(+47 -0)
api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseApiException.java 42(+42 -0)
api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransition.java 68(+68 -0)
beatrix/pom.xml 126(+55 -71)
beatrix/src/test/java/com/ning/billing/beatrix/integration/osgi/TestJrubyCurrencyPlugin.java 99(+0 -99)
beatrix/src/test/java/com/ning/billing/beatrix/integration/osgi/TestJrubyNotificationPlugin.java 65(+0 -65)
beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/IntegrationTestOverdueModule.java 33(+0 -33)
beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/MockOverdueService.java 46(+0 -46)
beatrix/src/test/java/com/ning/billing/beatrix/integration/overdue/TestBillingAlignment.java 80(+0 -80)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/BeatrixIntegrationModule.java 113(+57 -56)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestBasicOSGIWithTestBundle.java 20(+10 -10)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestJrubyCurrencyPlugin.java 99(+99 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestJrubyNotificationPlugin.java 62(+62 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestJrubyPaymentPlugin.java 24(+12 -12)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestOSGIIntegration.java 27(+27 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestPaymentOSGIWithTestPaymentBundle.java 38(+19 -19)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/IntegrationTestOverdueModule.java 33(+33 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/MockOverdueService.java 46(+46 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestBillingAlignment.java 80(+80 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java 88(+88 -0)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java 38(+19 -19)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueWithOverdueEnforcementOffTag.java 18(+9 -9)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueWithSubscriptionCancellation.java 26(+12 -14)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java 141(+59 -82)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java 36(+18 -18)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceOffTag.java 28(+14 -14)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java 34(+17 -17)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithDifferentBillingPeriods.java 24(+12 -12)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestRepairIntegration.java 36(+18 -18)
beatrix/src/test/resources/beatrix.properties 40(+20 -20)
bin/clean-and-install 2(+1 -1)
bin/cleanAndInstall 2(+1 -1)
bin/start-server 4(+2 -2)
catalog/pom.xml 28(+14 -14)
currency/pom.xml 52(+16 -36)
currency/src/main/java/com/ning/billing/currency/DefaultCurrencyProviderPluginRegistry.java 69(+0 -69)
currency/src/main/java/com/ning/billing/currency/glue/DefaultCurrencyProviderPluginRegistryProvider.java 39(+0 -39)
currency/src/main/java/org/killbill/billing/currency/api/DefaultCurrencyConversionApi.java 73(+73 -0)
currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyProviderPluginRegistry.java 69(+69 -0)
currency/src/main/java/org/killbill/billing/currency/glue/DefaultCurrencyProviderPluginRegistryProvider.java 39(+39 -0)
entitlement/pom.xml 84(+32 -52)
entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultBlockingTransitionInternalEvent.java 152(+0 -152)
entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEffectiveEntitlementEvent.java 166(+0 -166)
entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionBundle.java 86(+0 -86)
entitlement/src/main/java/com/ning/billing/entitlement/api/svcs/DefaultAccountEntitlements.java 90(+0 -90)
entitlement/src/main/java/com/ning/billing/entitlement/api/svcs/DefaultAccountEventsStreams.java 108(+0 -108)
entitlement/src/main/java/com/ning/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java 101(+0 -101)
entitlement/src/main/java/com/ning/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java 66(+0 -66)
entitlement/src/main/java/com/ning/billing/entitlement/dao/OptimizedProxyBlockingStateDao.java 101(+0 -101)
entitlement/src/main/java/com/ning/billing/entitlement/engine/core/BlockingTransitionNotificationKey.java 148(+0 -148)
entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementNotificationKey.java 108(+0 -108)
entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementNotificationKeyAction.java 24(+0 -24)
entitlement/src/main/java/com/ning/billing/entitlement/glue/DefaultEntitlementModule.java 87(+0 -87)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultBlockingTransitionInternalEvent.java 152(+152 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEffectiveEntitlementEvent.java 166(+166 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java 78(+39 -39)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionApi.java 44(+22 -22)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundle.java 86(+86 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java 26(+13 -13)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionEvent.java 12(+6 -6)
entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementDateHelper.java 116(+116 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEntitlements.java 90(+90 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEventsStreams.java 108(+108 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java 101(+101 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java 66(+66 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/block/DefaultBlockingChecker.java 26(+13 -13)
entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateModelDao.java 175(+175 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateSqlDao.java 102(+102 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/dao/DefaultBlockingStateDao.java 30(+15 -15)
entitlement/src/main/java/org/killbill/billing/entitlement/dao/OptimizedProxyBlockingStateDao.java 101(+101 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/dao/ProxyBlockingStateDao.java 36(+18 -18)
entitlement/src/main/java/org/killbill/billing/entitlement/DefaultEntitlementService.java 56(+28 -28)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/BlockingTransitionNotificationKey.java 148(+148 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java 36(+18 -18)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EntitlementNotificationKey.java 108(+108 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EntitlementNotificationKeyAction.java 24(+24 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EntitlementUtils.java 56(+28 -28)
entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EventsStreamBuilder.java 52(+26 -26)
entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementModule.java 87(+87 -0)
entitlement/src/main/resources/com/ning/billing/entitlement/dao/BlockingStateSqlDao.sql.stg 102(+0 -102)
entitlement/src/main/resources/org/killbill/billing/entitlement/dao/BlockingStateSqlDao.sql.stg 102(+102 -0)
entitlement/src/test/java/com/ning/billing/entitlement/api/TestEntitlementDateHelper.java 141(+0 -141)
entitlement/src/test/java/com/ning/billing/entitlement/EntitlementTestListenerStatus.java 59(+0 -59)
entitlement/src/test/java/com/ning/billing/entitlement/glue/TestEntitlementModuleNoDB.java 71(+0 -71)
entitlement/src/test/java/com/ning/billing/entitlement/glue/TestEntitlementModuleWithEmbeddedDB.java 58(+0 -58)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java 22(+11 -11)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java 26(+13 -13)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionApi.java 28(+14 -14)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java 28(+14 -14)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEntitlementDateHelper.java 141(+141 -0)
entitlement/src/test/java/org/killbill/billing/entitlement/block/MockBlockingChecker.java 51(+51 -0)
entitlement/src/test/java/org/killbill/billing/entitlement/block/TestBlockingChecker.java 24(+12 -12)
entitlement/src/test/java/org/killbill/billing/entitlement/dao/MockBlockingStateDao.java 106(+106 -0)
entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestDefaultBlockingStateDao.java 24(+12 -12)
entitlement/src/test/java/org/killbill/billing/entitlement/engine/core/TestEntitlementUtils.java 40(+20 -20)
entitlement/src/test/java/org/killbill/billing/entitlement/EntitlementTestSuiteWithEmbeddedDB.java 73(+32 -41)
entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModule.java 39(+39 -0)
entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModuleNoDB.java 71(+71 -0)
entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModuleWithEmbeddedDB.java 55(+55 -0)
invoice/pom.xml 80(+30 -50)
invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java 142(+0 -142)
invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java 83(+0 -83)
invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceAdjustmentEvent.java 96(+0 -96)
invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceCreationEvent.java 117(+0 -117)
invoice/src/main/java/com/ning/billing/invoice/notification/DefaultNextBillingDateNotifier.java 118(+0 -118)
invoice/src/main/java/com/ning/billing/invoice/notification/DefaultNextBillingDatePoster.java 95(+0 -95)
invoice/src/main/java/com/ning/billing/invoice/notification/NextBillingDateNotificationKey.java 32(+0 -32)
invoice/src/main/java/com/ning/billing/invoice/template/formatters/DefaultInvoiceFormatter.java 342(+0 -342)
invoice/src/main/java/com/ning/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java 33(+0 -33)
invoice/src/main/java/com/ning/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java 166(+0 -166)
invoice/src/main/java/com/ning/billing/invoice/template/translator/DefaultInvoiceTranslator.java 148(+0 -148)
invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java 142(+142 -0)
invoice/src/main/java/org/killbill/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java 83(+83 -0)
invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java 148(+148 -0)
invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceAdjustmentEvent.java 96(+96 -0)
invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceCreationEvent.java 117(+117 -0)
invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultNullInvoiceEvent.java 111(+111 -0)
invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java 194(+194 -0)
invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java 156(+156 -0)
invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java 259(+259 -0)
invoice/src/main/java/org/killbill/billing/invoice/model/CreditBalanceAdjInvoiceItem.java 58(+58 -0)
invoice/src/main/java/org/killbill/billing/invoice/model/InvalidDateSequenceException.java 15(+3 -12)
invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java 118(+118 -0)
invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java 95(+95 -0)
invoice/src/main/java/org/killbill/billing/invoice/notification/EmailInvoiceNotifier.java 106(+106 -0)
invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java 32(+32 -0)
invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotifier.java 30(+30 -0)
invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java 33(+33 -0)
invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java 342(+342 -0)
invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java 33(+33 -0)
invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java 166(+166 -0)
invoice/src/main/java/org/killbill/billing/invoice/template/translator/DefaultInvoiceTranslator.java 148(+148 -0)
invoice/src/main/java/org/killbill/billing/invoice/template/translator/InvoiceStrings.java 61(+61 -0)
invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg 101(+101 -0)
invoice/src/test/java/com/ning/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java 131(+0 -131)
invoice/src/test/java/com/ning/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java 134(+0 -134)
invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java 168(+0 -168)
invoice/src/test/java/com/ning/billing/invoice/generator/TestBillingIntervalDetail.java 168(+0 -168)
invoice/src/test/java/com/ning/billing/invoice/notification/MockNextBillingDateNotifier.java 35(+0 -35)
invoice/src/test/java/com/ning/billing/invoice/notification/MockNextBillingDatePoster.java 36(+0 -36)
invoice/src/test/java/com/ning/billing/invoice/notification/TestNextBillingDateNotifier.java 64(+0 -64)
invoice/src/test/java/com/ning/billing/invoice/template/formatters/TestDefaultInvoiceItemFormatter.java 119(+0 -119)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/annual/GenericProRationTests.java 37(+0 -37)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/annual/TestDoubleProRation.java 154(+0 -154)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/annual/TestLeadingProRation.java 153(+0 -153)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/annual/TestTrailingProRation.java 94(+0 -94)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/GenericProRationTestBase.java 188(+0 -188)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/monthly/GenericProRationTests.java 37(+0 -37)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/monthly/TestDoubleProRation.java 149(+0 -149)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/monthly/TestLeadingProRation.java 153(+0 -153)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/monthly/TestProRation.java 251(+0 -251)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/monthly/TestTrailingProRation.java 96(+0 -96)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/ProRationInAdvanceTestBase.java 29(+0 -29)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/quarterly/GenericProRationTests.java 37(+0 -37)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/quarterly/TestDoubleProRation.java 149(+0 -149)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/quarterly/TestLeadingProRation.java 153(+0 -153)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/quarterly/TestProRation.java 247(+0 -247)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/quarterly/TestTrailingProRation.java 94(+0 -94)
invoice/src/test/java/com/ning/billing/invoice/tests/inAdvance/TestValidationProRation.java 90(+0 -90)
invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java 131(+131 -0)
invoice/src/test/java/org/killbill/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java 134(+134 -0)
invoice/src/test/java/org/killbill/billing/invoice/api/user/TestDefaultInvoiceUserApi.java 34(+17 -17)
invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java 168(+168 -0)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestBillingIntervalDetail.java 168(+168 -0)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java 96(+48 -48)
invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModuleWithEmbeddedDb.java 54(+54 -0)
invoice/src/test/java/org/killbill/billing/invoice/model/TestExternalChargeInvoiceItem.java 70(+70 -0)
invoice/src/test/java/org/killbill/billing/invoice/notification/MockNextBillingDateNotifier.java 35(+35 -0)
invoice/src/test/java/org/killbill/billing/invoice/notification/MockNextBillingDatePoster.java 36(+36 -0)
invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java 64(+64 -0)
invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java 40(+20 -20)
invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceItemFormatter.java 119(+119 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/GenericProRationTests.java 37(+37 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestDoubleProRation.java 154(+154 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestLeadingProRation.java 153(+153 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestProRation.java 69(+69 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestTrailingProRation.java 94(+94 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/GenericProRationTestBase.java 188(+188 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/GenericProRationTests.java 37(+37 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestDoubleProRation.java 149(+149 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestLeadingProRation.java 153(+153 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestProRation.java 251(+251 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestTrailingProRation.java 96(+96 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/ProRationInAdvanceTestBase.java 29(+29 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/GenericProRationTests.java 37(+37 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestDoubleProRation.java 149(+149 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestLeadingProRation.java 153(+153 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestProRation.java 247(+247 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestTrailingProRation.java 94(+94 -0)
invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/TestValidationProRation.java 90(+90 -0)
invoice/src/test/resources/com/ning/billing/util/template/translation/InvoiceTranslation_en_US.properties 22(+0 -22)
invoice/src/test/resources/org/killbill/billing/util/template/translation/InvoiceTranslation_en_US.properties 22(+22 -0)
jaxrs/pom.xml 40(+20 -20)
jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/SubscriptionBillingApiExceptionMapper.java 42(+0 -42)
jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/SubscriptionRepairExceptionMapper.java 75(+0 -75)
jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EntitlementApiExceptionMapper.java 51(+51 -0)
jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EntityPersistenceExceptionMapper.java 42(+42 -0)
jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/IllegalArgumentExceptionMapper.java 41(+41 -0)
jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionApiExceptionMapper.java 89(+89 -0)
jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionBillingApiExceptionMapper.java 42(+42 -0)
jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionRepairExceptionMapper.java 75(+75 -0)
jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/TagDefinitionApiExceptionMapper.java 53(+53 -0)
junction/pom.xml 76(+28 -48)
junction/src/main/java/com/ning/billing/junction/plumbing/billing/BillCycleDayCalculator.java 153(+0 -153)
junction/src/main/java/com/ning/billing/junction/plumbing/billing/BlockingCalculator.java 312(+0 -312)
junction/src/main/java/com/ning/billing/junction/plumbing/billing/DefaultBillingEvent.java 325(+0 -325)
junction/src/main/java/com/ning/billing/junction/plumbing/billing/DefaultBillingEventSet.java 66(+0 -66)
junction/src/main/java/com/ning/billing/junction/plumbing/billing/DefaultInternalBillingApi.java 167(+0 -167)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java 153(+153 -0)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java 312(+312 -0)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java 325(+325 -0)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEventSet.java 66(+66 -0)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java 167(+167 -0)
junction/src/test/java/com/ning/billing/junction/glue/TestJunctionModuleWithEmbeddedDB.java 59(+0 -59)
junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java 141(+0 -141)
junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestDefaultBillingEvent.java 208(+0 -208)
junction/src/test/java/com/ning/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java 269(+0 -269)
junction/src/test/java/org/killbill/billing/junction/glue/TestJunctionModuleWithEmbeddedDB.java 56(+56 -0)
junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteWithEmbeddedDB.java 216(+216 -0)
junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java 141(+141 -0)
junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java 286(+286 -0)
junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java 36(+18 -18)
junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultBillingEvent.java 208(+208 -0)
junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java 269(+269 -0)
NEWS 3(+3 -0)
osgi/pom.xml 64(+32 -32)
osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginConfigServiceApi.java 56(+56 -0)
osgi-bundles/bundles/jruby/pom.xml 86(+43 -43)
osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyActivator.java 207(+0 -207)
osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyCurrencyPlugin.java 133(+0 -133)
osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyHttpServlet.java 40(+0 -40)
osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java 49(+0 -49)
osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java 212(+0 -212)
osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPlugin.java 297(+0 -297)
osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyActivator.java 207(+207 -0)
osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyCurrencyPlugin.java 133(+133 -0)
osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyHttpServlet.java 40(+40 -0)
osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java 49(+49 -0)
osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java 212(+212 -0)
osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPlugin.java 297(+297 -0)
osgi-bundles/bundles/logger/pom.xml 10(+5 -5)
osgi-bundles/bundles/logger/src/main/java/com/ning/billing/osgi/bundles/logger/Activator.java 100(+0 -100)
osgi-bundles/bundles/logger/src/main/java/com/ning/billing/osgi/bundles/logger/KillbillLogWriter.java 193(+0 -193)
osgi-bundles/bundles/logger/src/main/java/org/killbill/billing/osgi/bundles/logger/Activator.java 100(+100 -0)
osgi-bundles/bundles/logger/src/main/java/org/killbill/billing/osgi/bundles/logger/KillbillLogWriter.java 193(+193 -0)
osgi-bundles/bundles/meter/pom.xml 31(+8 -23)
osgi-bundles/bundles/meter/src/main/java/com/ning/billing/meter/jaxrs/resources/MeterResource.java 230(+0 -230)
osgi-bundles/bundles/meter/src/main/java/org/killbill/billing/meter/jaxrs/resources/MeterResource.java 230(+230 -0)
osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/timeline/aggregator/TestTimelineAggregator.java 169(+0 -169)
osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/timeline/chunks/TestTimelineChunk.java 73(+0 -73)
osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/timeline/consumer/TestAccumulatorSampleConsumer.java 51(+0 -51)
osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/timeline/persistent/TestFileBackedBuffer.java 149(+0 -149)
osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/timeline/persistent/TestSamplesReplayer.java 118(+0 -118)
osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/timeline/TestDateTimeUtils.java 41(+0 -41)
osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/timeline/TestInMemoryEventHandler.java 113(+0 -113)
osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/timeline/TestTimelineEventHandler.java 130(+0 -130)
osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/timeline/TestTimelineSourceEventAccumulator.java 97(+0 -97)
osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/timeline/TimelineLoadGenerator.java 212(+0 -212)
osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/aggregator/TestTimelineAggregator.java 169(+169 -0)
osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/chunks/TestTimelineChunk.java 73(+73 -0)
osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/consumer/TestAccumulatorSampleConsumer.java 51(+51 -0)
osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/persistent/TestFileBackedBuffer.java 149(+149 -0)
osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/persistent/TestSamplesReplayer.java 118(+118 -0)
osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestDateTimeUtils.java 41(+41 -0)
osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestInMemoryEventHandler.java 113(+113 -0)
osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestTimelineEventHandler.java 130(+130 -0)
osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestTimelineSourceEventAccumulator.java 97(+97 -0)
osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TimelineLoadGenerator.java 212(+212 -0)
osgi-bundles/bundles/pom.xml 4(+2 -2)
osgi-bundles/defaultbundles/pom.xml 28(+14 -14)
osgi-bundles/libs/killbill/pom.xml 16(+8 -8)
osgi-bundles/libs/killbill/src/main/java/com/ning/killbill/osgi/libs/killbill/KillbillActivatorBase.java 93(+0 -93)
osgi-bundles/libs/killbill/src/main/java/com/ning/killbill/osgi/libs/killbill/OSGIKillbillAPI.java 218(+0 -218)
osgi-bundles/libs/killbill/src/main/java/com/ning/killbill/osgi/libs/killbill/OSGIKillbillDataSource.java 50(+0 -50)
osgi-bundles/libs/killbill/src/main/java/com/ning/killbill/osgi/libs/killbill/OSGIKillbillEventDispatcher.java 95(+0 -95)
osgi-bundles/libs/killbill/src/main/java/com/ning/killbill/osgi/libs/killbill/OSGIKillbillLibraryBase.java 53(+0 -53)
osgi-bundles/libs/killbill/src/main/java/com/ning/killbill/osgi/libs/killbill/OSGIKillbillLogService.java 91(+0 -91)
osgi-bundles/libs/killbill/src/main/java/com/ning/killbill/osgi/libs/killbill/OSGIKillbillRegistrar.java 52(+0 -52)
osgi-bundles/libs/killbill/src/main/java/com/ning/killbill/osgi/libs/killbill/OSGIServiceNotAvailable.java 38(+0 -38)
osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/KillbillActivatorBase.java 93(+93 -0)
osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillAPI.java 218(+218 -0)
osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillDataSource.java 50(+50 -0)
osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillEventDispatcher.java 95(+95 -0)
osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillLibraryBase.java 53(+53 -0)
osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillLogService.java 91(+91 -0)
osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillRegistrar.java 52(+52 -0)
osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIServiceNotAvailable.java 38(+38 -0)
osgi-bundles/libs/pom.xml 4(+2 -2)
osgi-bundles/libs/slf4j-osgi/pom.xml 6(+3 -3)
osgi-bundles/pom.xml 4(+2 -2)
osgi-bundles/tests/beatrix/pom.xml 24(+12 -12)
osgi-bundles/tests/beatrix/src/main/java/org/killbill/billing/osgi/bundles/test/Dummy.java 21(+21 -0)
osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/dao/TestDao.java 89(+0 -89)
osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/TestActivator.java 106(+0 -106)
osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java 236(+0 -236)
osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/dao/TestDao.java 89(+89 -0)
osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestActivator.java 106(+106 -0)
osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java 236(+236 -0)
osgi-bundles/tests/payment/pom.xml 22(+11 -11)
osgi-bundles/tests/payment/src/main/java/org/killbill/billing/osgi/bundles/test/Dummy.java 21(+21 -0)
osgi-bundles/tests/payment/src/test/java/com/ning/billing/osgi/bundles/test/PaymentActivator.java 63(+0 -63)
osgi-bundles/tests/payment/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java 368(+0 -368)
osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/PaymentActivator.java 63(+63 -0)
osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java 368(+368 -0)
osgi-bundles/tests/pom.xml 4(+2 -2)
overdue/pom.xml 72(+26 -46)
overdue/src/main/java/com/ning/billing/overdue/applicator/DefaultOverdueChangeEvent.java 138(+0 -138)
overdue/src/main/java/com/ning/billing/overdue/applicator/formatters/DefaultBillingStateFormatter.java 37(+0 -37)
overdue/src/main/java/com/ning/billing/overdue/applicator/formatters/DefaultOverdueEmailFormatterFactory.java 27(+0 -27)
overdue/src/main/java/com/ning/billing/overdue/notification/DefaultOverdueNotifierBase.java 106(+0 -106)
overdue/src/main/java/com/ning/billing/overdue/notification/DefaultOverduePosterBase.java 140(+0 -140)
overdue/src/main/java/com/ning/billing/overdue/notification/OverdueAsyncBusNotificationKey.java 74(+0 -74)
overdue/src/main/java/com/ning/billing/overdue/notification/OverdueCheckNotificationKey.java 32(+0 -32)
overdue/src/main/java/org/killbill/billing/overdue/applicator/DefaultOverdueChangeEvent.java 138(+138 -0)
overdue/src/main/java/org/killbill/billing/overdue/applicator/formatters/DefaultBillingStateFormatter.java 37(+37 -0)
overdue/src/main/java/org/killbill/billing/overdue/applicator/formatters/DefaultOverdueEmailFormatterFactory.java 27(+27 -0)
overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java 379(+379 -0)
overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java 108(+108 -0)
overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverdueNotifierBase.java 106(+106 -0)
overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java 140(+140 -0)
overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusNotificationKey.java 74(+74 -0)
overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusNotifier.java 81(+81 -0)
overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusPoster.java 56(+56 -0)
overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckNotificationKey.java 32(+32 -0)
overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckNotifier.java 67(+67 -0)
overdue/src/test/java/com/ning/billing/overdue/applicator/formatters/TestDefaultBillingStateFormatter.java 40(+0 -40)
overdue/src/test/java/com/ning/billing/overdue/applicator/TestOverdueStateApplicator.java 85(+0 -85)
overdue/src/test/java/com/ning/billing/overdue/calculator/TestBillingStateCalculator.java 104(+0 -104)
overdue/src/test/java/com/ning/billing/overdue/notification/TestDefaultOverdueCheckPoster.java 99(+0 -99)
overdue/src/test/java/com/ning/billing/overdue/notification/TestOverdueCheckNotifier.java 117(+0 -117)
overdue/src/test/java/com/ning/billing/overdue/notification/TestOverdueNotificationKeyJson.java 53(+0 -53)
overdue/src/test/java/org/killbill/billing/overdue/applicator/formatters/TestDefaultBillingStateFormatter.java 40(+40 -0)
overdue/src/test/java/org/killbill/billing/overdue/applicator/OverdueBusListenerTester.java 49(+49 -0)
overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java 85(+85 -0)
overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java 104(+104 -0)
overdue/src/test/java/org/killbill/billing/overdue/glue/ApplicatorMockJunctionModule.java 70(+70 -0)
overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModuleWithEmbeddedDB.java 43(+43 -0)
overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java 99(+99 -0)
overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java 117(+117 -0)
overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueNotificationKeyJson.java 53(+53 -0)
payment/pom.xml 63(+21 -42)
payment/src/main/java/com/ning/billing/payment/glue/DefaultPaymentProviderPluginRegistryProvider.java 59(+0 -59)
payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java 160(+0 -160)
payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java 183(+0 -183)
payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java 263(+0 -263)
payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpRefundInfoPlugin.java 160(+0 -160)
payment/src/main/java/com/ning/billing/payment/provider/DefaultPaymentMethodInfoPlugin.java 60(+0 -60)
payment/src/main/java/com/ning/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java 76(+0 -76)
payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java 121(+0 -121)
payment/src/main/java/com/ning/billing/payment/provider/NoOpPaymentProviderPluginModule.java 36(+0 -36)
payment/src/main/java/com/ning/billing/payment/provider/NoOpPaymentProviderPluginProvider.java 62(+0 -62)
payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentPluginErrorEvent.java 106(+106 -0)
payment/src/main/java/org/killbill/billing/payment/api/svcs/DefaultPaymentInternalApi.java 68(+68 -0)
payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentProviderPluginRegistryProvider.java 59(+59 -0)
payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java 160(+160 -0)
payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java 183(+183 -0)
payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java 263(+263 -0)
payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpRefundInfoPlugin.java 160(+160 -0)
payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentMethodInfoPlugin.java 60(+60 -0)
payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java 76(+76 -0)
payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java 121(+121 -0)
payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginModule.java 36(+36 -0)
payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginProvider.java 62(+62 -0)
payment/src/main/java/org/killbill/billing/payment/retry/FailedPaymentRetryService.java 108(+108 -0)
payment/src/main/java/org/killbill/billing/payment/retry/PaymentRetryNotificationKey.java 31(+31 -0)
payment/src/main/java/org/killbill/billing/payment/retry/PluginFailureRetryService.java 111(+111 -0)
payment/src/test/java/com/ning/billing/payment/core/TestPaymentMethodProcessorRefreshWithDB.java 110(+0 -110)
payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPluginModule.java 41(+0 -41)
payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPluginProvider.java 61(+0 -61)
payment/src/test/java/com/ning/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java 53(+0 -53)
payment/src/test/java/com/ning/billing/payment/provider/TestDefaultNoOpPaymentMethodPlugin.java 63(+0 -63)
payment/src/test/java/com/ning/billing/payment/provider/TestExternalPaymentProviderPlugin.java 66(+0 -66)
payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java 62(+62 -0)
payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorRefreshWithDB.java 110(+110 -0)
payment/src/test/java/org/killbill/billing/payment/glue/TestPaymentModuleWithEmbeddedDB.java 37(+37 -0)
payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java 228(+228 -0)
payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPluginModule.java 41(+41 -0)
payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPluginProvider.java 61(+61 -0)
payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java 53(+53 -0)
payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentMethodPlugin.java 63(+63 -0)
payment/src/test/java/org/killbill/billing/payment/provider/TestExternalPaymentProviderPlugin.java 66(+66 -0)
pom.xml 6(+3 -3)
server/pom.xml 238(+126 -112)
server/src/deb/control/config 6(+3 -3)
server/src/deb/control/postinst 6(+3 -3)
server/src/deb/support/killbill.properties 26(+13 -13)
server/src/main/java/com/ning/billing/server/notifications/PushNotificationListener.java 114(+0 -114)
server/src/main/java/org/killbill/billing/server/modules/DataSourceConnectionPoolingType.java 22(+22 -0)
server/src/main/java/org/killbill/billing/server/notifications/PushNotificationListener.java 114(+114 -0)
server/src/main/resources/ehcache.xml 10(+5 -5)
server/src/main/resources/shiro.ini 4(+2 -2)
server/src/test/resources/killbill.properties 36(+17 -19)
subscription/pom.xml 80(+30 -50)
subscription/src/main/java/com/ning/billing/subscription/alignment/MigrationPlanAligner.java 210(+0 -210)
subscription/src/main/java/com/ning/billing/subscription/api/migration/AccountMigrationData.java 89(+0 -89)
subscription/src/main/java/com/ning/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java 276(+0 -276)
subscription/src/main/java/com/ning/billing/subscription/api/SubscriptionBaseApiService.java 71(+0 -71)
subscription/src/main/java/com/ning/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java 399(+0 -399)
subscription/src/main/java/com/ning/billing/subscription/api/timeline/DefaultDeletedEvent.java 42(+0 -42)
subscription/src/main/java/com/ning/billing/subscription/api/timeline/DefaultNewEvent.java 58(+0 -58)
subscription/src/main/java/com/ning/billing/subscription/api/timeline/DefaultRepairSubscriptionEvent.java 120(+0 -120)
subscription/src/main/java/com/ning/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java 320(+0 -320)
subscription/src/main/java/com/ning/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java 508(+0 -508)
subscription/src/main/java/com/ning/billing/subscription/api/timeline/RepairSubscriptionApiService.java 53(+0 -53)
subscription/src/main/java/com/ning/billing/subscription/api/timeline/RepairSubscriptionLifecycleDao.java 30(+0 -30)
subscription/src/main/java/com/ning/billing/subscription/api/timeline/SubscriptionDataRepair.java 215(+0 -215)
subscription/src/main/java/com/ning/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java 283(+0 -283)
subscription/src/main/java/com/ning/billing/subscription/api/transfer/TransferCancelData.java 40(+0 -40)
subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java 61(+0 -61)
subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java 67(+0 -67)
subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBase.java 659(+0 -659)
subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java 464(+0 -464)
subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBaseBundle.java 116(+0 -116)
subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionEvent.java 323(+0 -323)
subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionStatusDryRun.java 77(+0 -77)
subscription/src/main/java/com/ning/billing/subscription/api/user/SubscriptionBaseTransitionData.java 391(+0 -391)
subscription/src/main/java/com/ning/billing/subscription/api/user/SubscriptionBaseTransitionDataIterator.java 115(+0 -115)
subscription/src/main/java/com/ning/billing/subscription/api/user/SubscriptionBuilder.java 149(+0 -149)
subscription/src/main/java/com/ning/billing/subscription/api/user/SubscriptionEvents.java 101(+0 -101)
subscription/src/main/java/com/ning/billing/subscription/engine/core/DefaultSubscriptionBaseService.java 200(+0 -200)
subscription/src/main/java/com/ning/billing/subscription/engine/core/SubscriptionNotificationKey.java 92(+0 -92)
subscription/src/main/java/com/ning/billing/subscription/engine/dao/model/SubscriptionBundleModelDao.java 151(+0 -151)
subscription/src/main/java/com/ning/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java 341(+0 -341)
subscription/src/main/java/com/ning/billing/subscription/engine/dao/model/SubscriptionModelDao.java 196(+0 -196)
subscription/src/main/java/com/ning/billing/subscription/engine/dao/RepairSubscriptionDao.java 350(+0 -350)
subscription/src/main/java/com/ning/billing/subscription/engine/dao/SubscriptionDao.java 104(+0 -104)
subscription/src/main/java/com/ning/billing/subscription/engine/dao/SubscriptionEventSqlDao.java 63(+0 -63)
subscription/src/main/java/com/ning/billing/subscription/engine/dao/SubscriptionSqlDao.java 59(+0 -59)
subscription/src/main/java/com/ning/billing/subscription/events/phase/PhaseEventBuilder.java 42(+0 -42)
subscription/src/main/java/com/ning/billing/subscription/events/phase/PhaseEventData.java 70(+0 -70)
subscription/src/main/java/com/ning/billing/subscription/events/SubscriptionBaseEvent.java 54(+0 -54)
subscription/src/main/java/com/ning/billing/subscription/events/user/ApiEventBuilder.java 83(+0 -83)
subscription/src/main/java/com/ning/billing/subscription/events/user/ApiEventMigrateBilling.java 40(+0 -40)
subscription/src/main/java/com/ning/billing/subscription/events/user/ApiEventMigrateSubscription.java 24(+0 -24)
subscription/src/main/java/com/ning/billing/subscription/events/user/ApiEventReCreate.java 24(+0 -24)
subscription/src/main/java/com/ning/billing/subscription/events/user/ApiEventTransfer.java 23(+0 -23)
subscription/src/main/java/com/ning/billing/subscription/events/user/ApiEventUncancel.java 24(+0 -24)
subscription/src/main/java/com/ning/billing/subscription/exceptions/SubscriptionBaseError.java 38(+0 -38)
subscription/src/main/java/com/ning/billing/subscription/glue/DefaultSubscriptionModule.java 117(+0 -117)
subscription/src/main/java/org/killbill/billing/subscription/alignment/MigrationPlanAligner.java 210(+210 -0)
subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java 342(+342 -0)
subscription/src/main/java/org/killbill/billing/subscription/alignment/TimedMigration.java 126(+126 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/migration/AccountMigrationData.java 89(+89 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java 276(+276 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionApiBase.java 67(+67 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java 71(+71 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java 399(+399 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultDeletedEvent.java 42(+42 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultNewEvent.java 58(+58 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultRepairSubscriptionEvent.java 120(+120 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java 320(+320 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java 508(+508 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java 53(+53 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionLifecycleDao.java 30(+30 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionDataRepair.java 215(+215 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java 283(+283 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/transfer/TransferCancelData.java 40(+40 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java 61(+61 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java 67(+67 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java 659(+659 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java 464(+464 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseBundle.java 116(+116 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java 323(+323 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionStatusDryRun.java 77(+77 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java 391(+391 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionDataIterator.java 115(+115 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBuilder.java 149(+149 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionEvents.java 101(+101 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java 122(+122 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java 200(+200 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/core/EventListener.java 27(+27 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/core/SubscriptionNotificationKey.java 92(+92 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/BundleSqlDao.java 63(+63 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java 123(+64 -59)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionBundleModelDao.java 151(+151 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java 341(+341 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionModelDao.java 196(+196 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java 350(+350 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java 104(+104 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java 63(+63 -0)
subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionSqlDao.java 59(+59 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/EventBaseBuilder.java 144(+144 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEvent.java 25(+25 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventBuilder.java 42(+42 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java 70(+70 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java 54(+54 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBase.java 87(+87 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java 83(+83 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCancel.java 25(+25 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventChange.java 25(+25 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCreate.java 25(+25 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateBilling.java 40(+40 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateSubscription.java 24(+24 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventReCreate.java 24(+24 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventTransfer.java 23(+23 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventType.java 74(+74 -0)
subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUncancel.java 24(+24 -0)
subscription/src/main/java/org/killbill/billing/subscription/exceptions/SubscriptionBaseError.java 38(+38 -0)
subscription/src/main/java/org/killbill/billing/subscription/glue/DefaultSubscriptionModule.java 117(+117 -0)
subscription/src/main/resources/com/ning/billing/subscription/engine/dao/BundleSqlDao.sql.stg 95(+0 -95)
subscription/src/main/resources/com/ning/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg 113(+0 -113)
subscription/src/main/resources/com/ning/billing/subscription/engine/dao/SubscriptionSqlDao.sql.stg 74(+0 -74)
subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg 85(+85 -0)
subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg 113(+113 -0)
subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionSqlDao.sql.stg 74(+74 -0)
subscription/src/test/java/com/ning/billing/subscription/alignment/TestPlanAligner.java 257(+0 -257)
subscription/src/test/java/com/ning/billing/subscription/alignment/TestTimedMigration.java 54(+0 -54)
subscription/src/test/java/com/ning/billing/subscription/api/migration/TestMigration.java 302(+0 -302)
subscription/src/test/java/com/ning/billing/subscription/api/timeline/TestRepairBP.java 702(+0 -702)
subscription/src/test/java/com/ning/billing/subscription/api/timeline/TestRepairWithAO.java 726(+0 -726)
subscription/src/test/java/com/ning/billing/subscription/api/timeline/TestRepairWithError.java 421(+0 -421)
subscription/src/test/java/com/ning/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java 275(+0 -275)
subscription/src/test/java/com/ning/billing/subscription/api/transfer/TestTransfer.java 522(+0 -522)
subscription/src/test/java/com/ning/billing/subscription/api/user/TestSubscriptionHelper.java 684(+0 -684)
subscription/src/test/java/com/ning/billing/subscription/api/user/TestUserApiAddOn.java 502(+0 -502)
subscription/src/test/java/com/ning/billing/subscription/api/user/TestUserApiCancel.java 240(+0 -240)
subscription/src/test/java/com/ning/billing/subscription/api/user/TestUserApiChangePlan.java 452(+0 -452)
subscription/src/test/java/com/ning/billing/subscription/api/user/TestUserApiCreate.java 262(+0 -262)
subscription/src/test/java/com/ning/billing/subscription/api/user/TestUserApiError.java 175(+0 -175)
subscription/src/test/java/com/ning/billing/subscription/api/user/TestUserApiRecreate.java 109(+0 -109)
subscription/src/test/java/com/ning/billing/subscription/DefaultSubscriptionTestInitializer.java 151(+0 -151)
subscription/src/test/java/com/ning/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java 486(+0 -486)
subscription/src/test/java/com/ning/billing/subscription/engine/dao/MockSubscriptionDaoSql.java 38(+0 -38)
subscription/src/test/java/com/ning/billing/subscription/glue/TestDefaultSubscriptionModule.java 53(+0 -53)
subscription/src/test/java/com/ning/billing/subscription/glue/TestDefaultSubscriptionModuleNoDB.java 74(+0 -74)
subscription/src/test/java/com/ning/billing/subscription/glue/TestDefaultSubscriptionModuleWithEmbeddedDB.java 63(+0 -63)
subscription/src/test/java/com/ning/billing/subscription/SubscriptionTestInitializer.java 48(+0 -48)
subscription/src/test/java/com/ning/billing/subscription/SubscriptionTestListenerStatus.java 59(+0 -59)
subscription/src/test/java/com/ning/billing/subscription/SubscriptionTestSuiteNoDB.java 140(+0 -140)
subscription/src/test/java/com/ning/billing/subscription/SubscriptionTestSuiteWithEmbeddedDB.java 138(+0 -138)
subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java 257(+257 -0)
subscription/src/test/java/org/killbill/billing/subscription/alignment/TestTimedMigration.java 54(+54 -0)
subscription/src/test/java/org/killbill/billing/subscription/alignment/TestTimedPhase.java 41(+41 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/migration/TestMigration.java 302(+302 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairBP.java 702(+702 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithAO.java 726(+726 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithError.java 421(+421 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java 275(+275 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java 522(+522 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestSubscriptionHelper.java 677(+677 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java 492(+492 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java 236(+236 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java 430(+430 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java 262(+262 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java 175(+175 -0)
subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiRecreate.java 109(+109 -0)
subscription/src/test/java/org/killbill/billing/subscription/DefaultSubscriptionTestInitializer.java 148(+148 -0)
subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java 486(+486 -0)
subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoSql.java 38(+38 -0)
subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModule.java 50(+50 -0)
subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleNoDB.java 74(+74 -0)
subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleWithEmbeddedDB.java 63(+63 -0)
subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestInitializer.java 46(+46 -0)
subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteNoDB.java 137(+137 -0)
subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteWithEmbeddedDB.java 132(+132 -0)
tenant/pom.xml 66(+23 -43)
tenant/src/main/java/org/killbill/billing/tenant/security/KillbillCredentialsMatcher.java 42(+42 -0)
tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java 37(+37 -0)
usage/pom.xml 58(+19 -39)
util/pom.xml 123(+57 -66)
util/src/main/java/com/ning/billing/util/audit/DefaultAccountAuditLogsForObjectType.java 151(+0 -151)
util/src/main/java/com/ning/billing/util/customfield/api/DefaultCustomFieldCreationEvent.java 99(+0 -99)
util/src/main/java/com/ning/billing/util/customfield/api/DefaultCustomFieldDeletionEvent.java 100(+0 -100)
util/src/main/java/com/ning/billing/util/customfield/api/DefaultCustomFieldUserApi.java 152(+0 -152)
util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java 97(+0 -97)
util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoWrapperInvocationHandler.java 431(+0 -431)
util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInterceptorAdapter.java 39(+0 -39)
util/src/main/java/com/ning/billing/util/security/AopAllianceMethodInvocationAdapter.java 51(+0 -51)
util/src/main/java/com/ning/billing/util/security/PermissionAnnotationMethodInterceptor.java 28(+0 -28)
util/src/main/java/com/ning/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java 223(+0 -223)
util/src/main/java/com/ning/billing/util/security/shiro/realm/SkipSSLCheckSocketFactory.java 78(+0 -78)
util/src/main/java/com/ning/billing/util/tag/api/user/DefaultControlTagCreationEvent.java 125(+0 -125)
util/src/main/java/com/ning/billing/util/tag/api/user/DefaultControlTagDefinitionCreationEvent.java 98(+0 -98)
util/src/main/java/com/ning/billing/util/tag/api/user/DefaultControlTagDefinitionDeletionEvent.java 97(+0 -97)
util/src/main/java/com/ning/billing/util/tag/api/user/DefaultControlTagDeletionEvent.java 125(+0 -125)
util/src/main/java/com/ning/billing/util/tag/api/user/DefaultUserTagDefinitionCreationEvent.java 98(+0 -98)
util/src/main/java/com/ning/billing/util/tag/api/user/DefaultUserTagDefinitionDeletionEvent.java 98(+0 -98)
util/src/main/java/com/ning/billing/util/template/translation/DefaultCatalogTranslator.java 36(+0 -36)
util/src/main/java/com/ning/billing/util/template/translation/DefaultTranslatorBase.java 119(+0 -119)
util/src/main/java/org/killbill/billing/util/audit/DefaultAccountAuditLogsForObjectType.java 151(+151 -0)
util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcherProvider.java 72(+72 -0)
util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java 262(+262 -0)
util/src/main/java/org/killbill/billing/util/callcontext/InternalTenantContextBinder.java 81(+81 -0)
util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldCreationEvent.java 99(+99 -0)
util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldDeletionEvent.java 100(+100 -0)
util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldUserApi.java 152(+152 -0)
util/src/main/java/org/killbill/billing/util/customfield/dao/DefaultCustomFieldDao.java 163(+163 -0)
util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java 66(+66 -0)
util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java 144(+144 -0)
util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java 97(+97 -0)
util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionWrapper.java 31(+31 -0)
util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperInvocationHandler.java 431(+431 -0)
util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java 130(+130 -0)
util/src/main/java/org/killbill/billing/util/security/AopAllianceMethodInterceptorAdapter.java 39(+39 -0)
util/src/main/java/org/killbill/billing/util/security/AopAllianceMethodInvocationAdapter.java 51(+51 -0)
util/src/main/java/org/killbill/billing/util/security/PermissionAnnotationMethodInterceptor.java 28(+28 -0)
util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java 223(+223 -0)
util/src/main/java/org/killbill/billing/util/security/shiro/realm/SkipSSLCheckSocketFactory.java 78(+78 -0)
util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagCreationEvent.java 125(+125 -0)
util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDefinitionCreationEvent.java 98(+98 -0)
util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDefinitionDeletionEvent.java 97(+97 -0)
util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDeletionEvent.java 125(+125 -0)
util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagCreationEvent.java 125(+125 -0)
util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDefinitionCreationEvent.java 98(+98 -0)
util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDefinitionDeletionEvent.java 98(+98 -0)
util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDeletionEvent.java 124(+124 -0)
util/src/main/java/org/killbill/billing/util/template/translation/DefaultCatalogTranslator.java 36(+36 -0)
util/src/main/java/org/killbill/billing/util/template/translation/DefaultTranslatorBase.java 119(+119 -0)
util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestBase.java 167(+167 -0)
util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestNotifier.java 28(+28 -0)
util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestWaiter.java 51(+51 -0)
util/src/main/resources/com/ning/billing/util/security/shiro/dao/JDBCSessionSqlDao.sql.stg 51(+0 -51)
util/src/main/resources/com/ning/billing/util/validation/dao/DatabaseSchemaSqlDao.sql.stg 10(+0 -10)
util/src/main/resources/ehcache.xml 10(+5 -5)
util/src/main/resources/org/killbill/billing/util/customfield/dao/CustomFieldSqlDao.sql.stg 60(+60 -0)
util/src/main/resources/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.sql.stg 51(+51 -0)
util/src/main/resources/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.sql.stg 10(+10 -0)
util/src/test/java/com/ning/billing/payment/plugin/api/PaymentPluginApiWithTestControl.java 26(+0 -26)
util/src/test/java/com/ning/billing/util/callcontext/TestInternalCallContextFactory.java 102(+0 -102)
util/src/test/java/com/ning/billing/util/customfield/api/TestDefaultCustomFieldCreationEvent.java 62(+0 -62)
util/src/test/java/com/ning/billing/util/customfield/api/TestDefaultCustomFieldDeletionEvent.java 64(+0 -64)
util/src/test/java/com/ning/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java 96(+0 -96)
util/src/test/java/com/ning/billing/util/security/shiro/realm/TestKillBillJndiLdapRealm.java 89(+0 -89)
util/src/test/java/com/ning/billing/util/security/TestPermissionAnnotationMethodInterceptor.java 146(+0 -146)
util/src/test/java/com/ning/billing/util/tag/api/user/TestDefaultControlTagCreationEvent.java 82(+0 -82)
util/src/test/java/com/ning/billing/util/tag/api/user/TestDefaultControlTagDefinitionCreationEvent.java 73(+0 -73)
util/src/test/java/com/ning/billing/util/tag/api/user/TestDefaultControlTagDefinitionDeletionEvent.java 73(+0 -73)
util/src/test/java/com/ning/billing/util/tag/api/user/TestDefaultControlTagDeletionEvent.java 82(+0 -82)
util/src/test/java/com/ning/billing/util/tag/api/user/TestDefaultUserTagCreationEvent.java 82(+0 -82)
util/src/test/java/com/ning/billing/util/tag/api/user/TestDefaultUserTagDefinitionCreationEvent.java 74(+0 -74)
util/src/test/java/com/ning/billing/util/tag/api/user/TestDefaultUserTagDefinitionDeletionEvent.java 73(+0 -73)
util/src/test/java/com/ning/billing/util/tag/api/user/TestDefaultUserTagDeletionEvent.java 82(+0 -82)
util/src/test/java/com/ning/billing/util/template/translation/TestDefaultTranslatorBase.java 53(+0 -53)
util/src/test/java/org/killbill/billing/payment/plugin/api/PaymentPluginApiWithTestControl.java 26(+26 -0)
util/src/test/java/org/killbill/billing/util/callcontext/TestInternalCallContextFactory.java 102(+102 -0)
util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldCreationEvent.java 62(+62 -0)
util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldDeletionEvent.java 64(+64 -0)
util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java 96(+96 -0)
util/src/test/java/org/killbill/billing/util/customfield/MockCustomFieldModuleMemory.java 28(+28 -0)
util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritanceWithJdbi.java 52(+52 -0)
util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJndiLdapRealm.java 89(+89 -0)
util/src/test/java/org/killbill/billing/util/security/TestPermissionAnnotationMethodInterceptor.java 146(+146 -0)
util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagCreationEvent.java 82(+82 -0)
util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDefinitionCreationEvent.java 73(+73 -0)
util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDefinitionDeletionEvent.java 73(+73 -0)
util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDeletionEvent.java 82(+82 -0)
util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagCreationEvent.java 82(+82 -0)
util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDefinitionCreationEvent.java 74(+74 -0)
util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDefinitionDeletionEvent.java 73(+73 -0)
util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDeletionEvent.java 82(+82 -0)
util/src/test/java/org/killbill/billing/util/template/translation/TestDefaultTranslatorBase.java 53(+53 -0)
util/src/test/resources/com/ning/billing/util/email/templates/HtmlInvoiceTemplate.mustache 96(+0 -96)
util/src/test/resources/com/ning/billing/util/template/translation/CatalogTranslation_en_US.properties 2(+0 -2)
util/src/test/resources/com/ning/billing/util/template/translation/CatalogTranslation_fr_CA.properties 2(+0 -2)
util/src/test/resources/org/killbill/billing/util/email/templates/HtmlInvoiceTemplate.mustache 96(+96 -0)
Details
.gitignore 4(+4 -0)
diff --git a/.gitignore b/.gitignore
index 9e14eea..c26b0f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ pom.xml.bak
pom.xml.releaseBackup
release.properties
logs/
+.logs
.diskspool
*.classpath
*.settings
@@ -24,3 +25,6 @@ catalog/src/test/resources/CatalogSchema.xsd
server/load
dependency-reduced-pom.xml
dependency-reduced-pom.xml.bak
+server/killbill.h2.db
+server/killbill.lock.db
+server/killbill.trace.db
.idea/codeStyleSettings.xml 200(+0 -200)
diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml
index 3c91252..0a01d27 100644
--- a/.idea/codeStyleSettings.xml
+++ b/.idea/codeStyleSettings.xml
@@ -53,206 +53,6 @@
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
- <arrangement>
- <groups>
- <group>
- <type>GETTERS_AND_SETTERS</type>
- <order>KEEP</order>
- </group>
- </groups>
- <rules>
- <rule>
- <match>
- <AND>
- <FIELD />
- <FINAL />
- <PUBLIC />
- <STATIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <FINAL />
- <PROTECTED />
- <STATIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <FINAL />
- <PACKAGE_PRIVATE />
- <STATIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <FINAL />
- <PRIVATE />
- <STATIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <PUBLIC />
- <STATIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <PROTECTED />
- <STATIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <PACKAGE_PRIVATE />
- <STATIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <PRIVATE />
- <STATIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <FINAL />
- <PUBLIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <FINAL />
- <PROTECTED />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <FINAL />
- <PACKAGE_PRIVATE />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <FINAL />
- <PRIVATE />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <PUBLIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <PROTECTED />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <PACKAGE_PRIVATE />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <FIELD />
- <PRIVATE />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <FIELD />
- </match>
- </rule>
- <rule>
- <match>
- <CONSTRUCTOR />
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <METHOD />
- <STATIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <METHOD />
- </match>
- </rule>
- <rule>
- <match>
- <ENUM />
- </match>
- </rule>
- <rule>
- <match>
- <INTERFACE />
- </match>
- </rule>
- <rule>
- <match>
- <AND>
- <CLASS />
- <STATIC />
- </AND>
- </match>
- </rule>
- <rule>
- <match>
- <CLASS />
- </match>
- </rule>
- </rules>
- </arrangement>
</codeStyleSettings>
</value>
</option>
.idea/modules.xml 1(+0 -1)
diff --git a/.idea/modules.xml b/.idea/modules.xml
index 0c74650..21e6df3 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,6 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
- <module fileurl="file://$PROJECT_DIR$/currency/currency.iml" filepath="$PROJECT_DIR$/currency/currency.iml" />
<module fileurl="file://$PROJECT_DIR$/killbill.iml" filepath="$PROJECT_DIR$/killbill.iml" />
<module fileurl="file://$PROJECT_DIR$/account/killbill-account.iml" filepath="$PROJECT_DIR$/account/killbill-account.iml" />
<module fileurl="file://$PROJECT_DIR$/beatrix/killbill-beatrix.iml" filepath="$PROJECT_DIR$/beatrix/killbill-beatrix.iml" />
.idea/vcs.xml 2(+1 -1)
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index def6a6a..c80f219 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
- <mapping directory="" vcs="" />
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
account/pom.xml 70(+25 -45)
diff --git a/account/pom.xml b/account/pom.xml
index 7759bc4..9b34bea 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-account</artifactId>
@@ -49,76 +49,56 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.antlr</groupId>
+ <artifactId>stringtemplate</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>javax.inject</groupId>
- <artifactId>javax.inject</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.antlr</groupId>
- <artifactId>stringtemplate</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
api/pom.xml 28(+14 -14)
diff --git a/api/pom.xml b/api/pom.xml
index 8a103fe..cec465b 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-internal-api</artifactId>
@@ -32,29 +32,29 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-api</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-clock</artifactId>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-queue</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-payment</artifactId>
</dependency>
<dependency>
- <groupId>javax.servlet</groupId>
- <artifactId>javax.servlet-api</artifactId>
- <scope>provided</scope>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
<groupId>org.skife.config</groupId>
diff --git a/api/src/main/java/org/killbill/billing/events/PaymentInfoInternalEvent.java b/api/src/main/java/org/killbill/billing/events/PaymentInfoInternalEvent.java
new file mode 100644
index 0000000..ac59f9d
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/events/PaymentInfoInternalEvent.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.events;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.payment.api.PaymentStatus;
+
+public interface PaymentInfoInternalEvent extends BusInternalEvent {
+
+ public UUID getPaymentId();
+
+ public UUID getAccountId();
+
+ public UUID getInvoiceId();
+
+ public BigDecimal getAmount();
+
+ public DateTime getEffectiveDate();
+
+ public Integer getPaymentNumber();
+
+ public PaymentStatus getStatus();
+}
diff --git a/api/src/main/java/org/killbill/billing/events/PaymentPluginErrorInternalEvent.java b/api/src/main/java/org/killbill/billing/events/PaymentPluginErrorInternalEvent.java
new file mode 100644
index 0000000..7314160
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/events/PaymentPluginErrorInternalEvent.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.events;
+
+import java.util.UUID;
+
+
+public interface PaymentPluginErrorInternalEvent extends BusInternalEvent {
+
+ public String getMessage();
+
+ public UUID getInvoiceId();
+
+ public UUID getAccountId();
+
+ public UUID getPaymentId();
+}
diff --git a/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java b/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java
new file mode 100644
index 0000000..ea74974
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/events/SubscriptionInternalEvent.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.events;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+
+public interface SubscriptionInternalEvent extends BusInternalEvent {
+ UUID getId();
+
+ SubscriptionBaseTransitionType getTransitionType();
+
+ UUID getBundleId();
+
+ UUID getSubscriptionId();
+
+ DateTime getSubscriptionStartDate();
+
+ DateTime getRequestedTransitionTime();
+
+ DateTime getEffectiveTransitionTime();
+
+ EntitlementState getPreviousState();
+
+ String getPreviousPlan();
+
+ String getPreviousPriceList();
+
+ String getPreviousPhase();
+
+ String getNextPlan();
+
+ String getNextPhase();
+
+ EntitlementState getNextState();
+
+ String getNextPriceList();
+
+ Integer getRemainingEventsForUserOperation();
+
+ Long getTotalOrdering();
+}
diff --git a/api/src/main/java/org/killbill/billing/events/TagDefinitionInternalEvent.java b/api/src/main/java/org/killbill/billing/events/TagDefinitionInternalEvent.java
new file mode 100644
index 0000000..f4102c5
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/events/TagDefinitionInternalEvent.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.events;
+
+import java.util.UUID;
+
+import org.killbill.billing.util.tag.TagDefinition;
+
+public interface TagDefinitionInternalEvent extends BusInternalEvent {
+ UUID getTagDefinitionId();
+
+ TagDefinition getTagDefinition();
+}
diff --git a/api/src/main/java/org/killbill/billing/events/TagInternalEvent.java b/api/src/main/java/org/killbill/billing/events/TagInternalEvent.java
new file mode 100644
index 0000000..7355ea2
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/events/TagInternalEvent.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.events;
+
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public interface TagInternalEvent extends BusInternalEvent {
+
+ UUID getTagId();
+
+ UUID getObjectId();
+
+ ObjectType getObjectType();
+
+ TagDefinition getTagDefinition();
+}
diff --git a/api/src/main/java/org/killbill/billing/events/UserTagDefinitionCreationInternalEvent.java b/api/src/main/java/org/killbill/billing/events/UserTagDefinitionCreationInternalEvent.java
new file mode 100644
index 0000000..575c244
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/events/UserTagDefinitionCreationInternalEvent.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.events;
+
+
+public interface UserTagDefinitionCreationInternalEvent extends TagDefinitionInternalEvent {
+}
diff --git a/api/src/main/java/org/killbill/billing/events/UserTagDefinitionDeletionInternalEvent.java b/api/src/main/java/org/killbill/billing/events/UserTagDefinitionDeletionInternalEvent.java
new file mode 100644
index 0000000..0a48595
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/events/UserTagDefinitionDeletionInternalEvent.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.events;
+
+
+public interface UserTagDefinitionDeletionInternalEvent extends TagDefinitionInternalEvent {
+}
diff --git a/api/src/main/java/org/killbill/billing/events/UserTagDeletionInternalEvent.java b/api/src/main/java/org/killbill/billing/events/UserTagDeletionInternalEvent.java
new file mode 100644
index 0000000..6872d57
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/events/UserTagDeletionInternalEvent.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.events;
+
+
+public interface UserTagDeletionInternalEvent extends TagInternalEvent {
+}
diff --git a/api/src/main/java/org/killbill/billing/glue/AccountModule.java b/api/src/main/java/org/killbill/billing/glue/AccountModule.java
new file mode 100644
index 0000000..80a68ae
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/glue/AccountModule.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.glue;
+
+
+
+public interface AccountModule {
+
+ public void installAccountUserApi();
+
+ public void installInternalApi();
+}
diff --git a/api/src/main/java/org/killbill/billing/glue/EntitlementModule.java b/api/src/main/java/org/killbill/billing/glue/EntitlementModule.java
new file mode 100644
index 0000000..db052aa
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/glue/EntitlementModule.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.glue;
+
+public interface EntitlementModule {
+
+ public void installBlockingStateDao();
+
+ public void installBlockingApi();
+
+ public void installEntitlementApi();
+
+ public void installEntitlementInternalApi();
+
+ public void installSubscriptionApi();
+
+ public void installBlockingChecker();
+}
diff --git a/api/src/main/java/org/killbill/billing/glue/InvoiceModule.java b/api/src/main/java/org/killbill/billing/glue/InvoiceModule.java
new file mode 100644
index 0000000..ed46527
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/glue/InvoiceModule.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.glue;
+
+public interface InvoiceModule {
+
+ public abstract void installInvoiceUserApi();
+
+ public abstract void installInvoicePaymentApi();
+
+ public abstract void installInvoiceMigrationApi();
+
+ public abstract void installInvoiceInternalApi();
+}
diff --git a/api/src/main/java/org/killbill/billing/glue/JunctionModule.java b/api/src/main/java/org/killbill/billing/glue/JunctionModule.java
new file mode 100644
index 0000000..74fd8b7
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/glue/JunctionModule.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.glue;
+
+
+public interface JunctionModule {
+ public void installBillingApi();
+}
diff --git a/api/src/main/java/org/killbill/billing/glue/OverdueModule.java b/api/src/main/java/org/killbill/billing/glue/OverdueModule.java
new file mode 100644
index 0000000..7dc786b
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/glue/OverdueModule.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.glue;
+
+public interface OverdueModule {
+
+ public abstract void installOverdueUserApi();
+
+}
diff --git a/api/src/main/java/org/killbill/billing/glue/SubscriptionModule.java b/api/src/main/java/org/killbill/billing/glue/SubscriptionModule.java
new file mode 100644
index 0000000..d561355
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/glue/SubscriptionModule.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.glue;
+
+public interface SubscriptionModule {
+
+ public void installSubscriptionService();
+
+ public void installSubscriptionTransferApi();
+
+ public void installSubscriptionMigrationApi();
+
+ public void installSubscriptionInternalApi();
+
+ public void installSubscriptionTimelineApi();
+}
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/formatters/InvoiceFormatterFactory.java b/api/src/main/java/org/killbill/billing/invoice/api/formatters/InvoiceFormatterFactory.java
new file mode 100644
index 0000000..3a2cdff
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/invoice/api/formatters/InvoiceFormatterFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.formatters;
+
+import java.util.Locale;
+
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+public interface InvoiceFormatterFactory {
+ public InvoiceFormatter createInvoiceFormatter(TranslatorConfig config, Invoice invoice, Locale locale, CurrencyConversionApi currencyConversionApi);
+}
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
new file mode 100644
index 0000000..b9bd129
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceInternalApi.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Map;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+public interface InvoiceInternalApi {
+
+ public Invoice getInvoiceById(UUID invoiceId, InternalTenantContext context) throws InvoiceApiException;
+
+ public Collection<Invoice> getUnpaidInvoicesByAccountId(UUID accountId, LocalDate upToDate, InternalTenantContext context);
+
+ public BigDecimal getAccountBalance(UUID accountId, InternalTenantContext context);
+
+ public void notifyOfPayment(UUID invoiceId, BigDecimal amountOutstanding, Currency currency, Currency processedCurrency, UUID paymentId, DateTime paymentDate, InternalCallContext context) throws InvoiceApiException;
+
+ public void notifyOfPayment(InvoicePayment invoicePayment, InternalCallContext context) throws InvoiceApiException;
+
+ public InvoicePayment getInvoicePaymentForAttempt(UUID paymentId, InternalTenantContext context) throws InvoiceApiException;
+
+ public Invoice getInvoiceForPaymentId(UUID paymentId, InternalTenantContext context) throws InvoiceApiException;
+
+ /**
+ * Create a refund.
+ *
+ * @param paymentId payment associated with that refund
+ * @param amount amount to refund
+ * @param isInvoiceAdjusted whether the refund should trigger an invoice or invoice item adjustment
+ * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
+ * @param paymentCookieId payment cookie id
+ * @param context the call callcontext
+ * @return the created invoice payment object associated with this refund
+ * @throws InvoiceApiException
+ */
+ public InvoicePayment createRefund(UUID paymentId, BigDecimal amount, boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts,
+ UUID paymentCookieId, InternalCallContext context) throws InvoiceApiException;
+
+ /**
+ * Rebalance CBA for account which have credit and unpaid invoices
+ *
+ * @param accountId account id
+ * @param context the callcontext
+ */
+ public void consumeExistingCBAOnAccountWithUnpaidInvoices(final UUID accountId, final InternalCallContext context) throws InvoiceApiException;
+
+}
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceMigrationApi.java b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceMigrationApi.java
new file mode 100644
index 0000000..4118ae9
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceMigrationApi.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.util.callcontext.CallContext;
+
+public interface InvoiceMigrationApi {
+
+ /**
+ * @param accountId account id
+ * @param targetDate maximum billing event day to consider (in the account timezone)
+ * @param balance invoice balance
+ * @param currency invoice currency
+ * @param context call callcontext
+ * @return The UUID of the created invoice
+ */
+ public UUID createMigrationInvoice(UUID accountId,
+ LocalDate targetDate,
+ BigDecimal balance,
+ Currency currency,
+ CallContext context);
+}
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceNotifier.java b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceNotifier.java
new file mode 100644
index 0000000..edcfb60
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceNotifier.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+public interface InvoiceNotifier {
+
+ public void notify(Account account, Invoice invoice, TenantContext tenantContext) throws InvoiceApiException;
+}
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/InvoiceService.java b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceService.java
new file mode 100644
index 0000000..9953104
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/invoice/api/InvoiceService.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api;
+
+import org.killbill.billing.lifecycle.KillbillService;
+
+public interface InvoiceService extends KillbillService {
+
+}
diff --git a/api/src/main/java/org/killbill/billing/junction/BillingEvent.java b/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
new file mode 100644
index 0000000..a916829
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/junction/BillingEvent.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction;
+
+import java.math.BigDecimal;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+
+public interface BillingEvent extends Comparable<BillingEvent> {
+
+ /**
+ * @return the account that this billing event is associated with
+ */
+ public Account getAccount();
+
+ /**
+ * @return the billCycleDay in the account timezone as seen for that subscription at that time
+ * <p/>
+ * Note: The billCycleDay may come from the Account, or the bundle or the subscription itself
+ */
+ public int getBillCycleDayLocal();
+
+ /**
+ * @return the subscription
+ */
+ public SubscriptionBase getSubscription();
+
+ /**
+ * @return the date for when that event became effective
+ */
+ public DateTime getEffectiveDate();
+
+ /**
+ * @return the plan phase
+ */
+ public PlanPhase getPlanPhase();
+
+ /**
+ * @return the plan
+ */
+ public Plan getPlan();
+
+ /**
+ * @return the billing period for the active phase
+ */
+ public BillingPeriod getBillingPeriod();
+
+ /**
+ * @return the billing mode for the current event
+ */
+ public BillingModeType getBillingMode();
+
+ /**
+ * @return the description of the billing event
+ */
+ public String getDescription();
+
+ /**
+ * @return the fixed price for the phase
+ */
+ public BigDecimal getFixedPrice();
+
+ /**
+ * @return the recurring price for the phase
+ */
+ public BigDecimal getRecurringPrice();
+
+ /**
+ * @return the currency for the account being invoiced
+ */
+ public Currency getCurrency();
+
+ /**
+ * @return the transition type of the underlying subscription event that triggered this
+ */
+ public SubscriptionBaseTransitionType getTransitionType();
+
+ /**
+ * @return a unique long indicating the ordering on which events got inserted on disk-- used for sorting only
+ */
+ public Long getTotalOrdering();
+
+ /**
+ * @return The TimeZone of the account
+ */
+ public DateTimeZone getTimeZone();
+}
diff --git a/api/src/main/java/org/killbill/billing/junction/BillingEventSet.java b/api/src/main/java/org/killbill/billing/junction/BillingEventSet.java
new file mode 100644
index 0000000..bfdbb0a
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/junction/BillingEventSet.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction;
+
+import java.util.List;
+import java.util.SortedSet;
+import java.util.UUID;
+
+public interface BillingEventSet extends SortedSet<BillingEvent> {
+
+ public abstract boolean isAccountAutoInvoiceOff();
+
+ public abstract List<UUID> getSubscriptionIdsWithAutoInvoiceOff();
+}
diff --git a/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java b/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
new file mode 100644
index 0000000..614b32f
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/junction/BillingInternalApi.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction;
+
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+
+public interface BillingInternalApi {
+
+ /**
+ * @return an ordered list of billing event for the given accounts
+ */
+ public BillingEventSet getBillingEventsForAccountAndUpdateAccountBCD(UUID accountId, InternalCallContext context);
+}
diff --git a/api/src/main/java/org/killbill/billing/junction/BillingModeType.java b/api/src/main/java/org/killbill/billing/junction/BillingModeType.java
new file mode 100644
index 0000000..b8a29c4
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/junction/BillingModeType.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction;
+
+public enum BillingModeType {
+ IN_ADVANCE
+}
diff --git a/api/src/main/java/org/killbill/billing/junction/BlockingInternalApi.java b/api/src/main/java/org/killbill/billing/junction/BlockingInternalApi.java
new file mode 100644
index 0000000..2ca6348
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/junction/BlockingInternalApi.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+
+public interface BlockingInternalApi {
+
+ public BlockingState getBlockingStateForService(UUID blockableId, BlockingStateType blockingStateType, String serviceName, InternalTenantContext context);
+
+ public List<BlockingState> getBlockingAllForAccount(InternalTenantContext context);
+
+ public void setBlockingState(BlockingState state, InternalCallContext context);
+}
diff --git a/api/src/main/java/org/killbill/billing/lifecycle/KillbillService.java b/api/src/main/java/org/killbill/billing/lifecycle/KillbillService.java
new file mode 100644
index 0000000..7adfaf0
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/lifecycle/KillbillService.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.lifecycle;
+
+/**
+ * The interface <code>KillbillService<code/> represents a service that will go through the Killbill lifecyle.
+ * <p>
+ * A <code>KillbillService<code> can register handlers for the various phases of the lifecycle, so
+ * that its proper initialization/shutdown sequence occurs at the right time with regard
+ * to other <code>KillbillService</code>.
+ *
+ */
+public interface KillbillService {
+
+ public static class ServiceException extends Exception {
+
+ private static final long serialVersionUID = 176191207L;
+
+ public ServiceException() {
+ super();
+ }
+
+ public ServiceException(final String msg, final Throwable e) {
+ super(msg, e);
+ }
+
+ public ServiceException(final String msg) {
+ super(msg);
+ }
+
+ public ServiceException(final Throwable msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * @return the name of the service
+ */
+ public String getName();
+
+
+}
diff --git a/api/src/main/java/org/killbill/billing/lifecycle/LifecycleHandlerType.java b/api/src/main/java/org/killbill/billing/lifecycle/LifecycleHandlerType.java
new file mode 100644
index 0000000..a1bf5ff
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/lifecycle/LifecycleHandlerType.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.lifecycle;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface LifecycleHandlerType {
+
+
+ //
+ // The level themselves are still work in progress depending on what we really need
+ //
+ // Ordering is important in that enum
+ //
+ public enum LifecycleLevel {
+
+ /**
+ * Load and validate catalog (only for catalog subsytem)
+ */
+ LOAD_CATALOG(Sequence.STARTUP_PRE_EVENT_REGISTRATION),
+ /**
+ * Initialize event bus (only for the event bus)
+ */
+ INIT_BUS(Sequence.STARTUP_PRE_EVENT_REGISTRATION),
+ /**
+ * Start Felix Framework along with its system bundle
+ */
+ INIT_PLUGIN(Sequence.STARTUP_PRE_EVENT_REGISTRATION),
+ /**
+ * Service specific initalization-- service does not start yet
+ */
+ INIT_SERVICE(Sequence.STARTUP_PRE_EVENT_REGISTRATION),
+ /**
+ * Start all the plugins
+ */
+ START_PLUGIN(Sequence.STARTUP_PRE_EVENT_REGISTRATION),
+ /**
+ * Service start
+ * - API call should not work
+ * - Events might be triggered
+ * - Batch processing jobs started
+ */
+ START_SERVICE(Sequence.STARTUP_POST_EVENT_REGISTRATION),
+ /**
+ * Stop service
+ */
+ STOP_SERVICE(Sequence.SHUTDOWN_PRE_EVENT_UNREGISTRATION),
+ /**
+ * Stop the plugins
+ */
+ STOP_PLUGIN(Sequence.SHUTDOWN_PRE_EVENT_UNREGISTRATION),
+ /**
+ * Stop bus
+ */
+ STOP_BUS(Sequence.SHUTDOWN_POST_EVENT_UNREGISTRATION),
+ /**
+ * Any service specific shutdown action before the end
+ */
+ SHUTDOWN(Sequence.SHUTDOWN_POST_EVENT_UNREGISTRATION);
+
+ public enum Sequence {
+ STARTUP_PRE_EVENT_REGISTRATION,
+ STARTUP_POST_EVENT_REGISTRATION,
+ SHUTDOWN_PRE_EVENT_UNREGISTRATION,
+ SHUTDOWN_POST_EVENT_UNREGISTRATION
+ }
+
+ private final Sequence seq;
+
+ LifecycleLevel(final Sequence seq) {
+ this.seq = seq;
+ }
+
+ public Sequence getSequence() {
+ return seq;
+ }
+
+ //
+ // Returns an ordered list of level for a particular sequence
+ //
+ public static List<LifecycleLevel> getLevelsForSequence(final Sequence seq) {
+ final List<LifecycleLevel> result = new ArrayList<LifecycleLevel>();
+ for (final LifecycleLevel level : LifecycleLevel.values()) {
+ if (level.getSequence() == seq) {
+ result.add(level);
+ }
+ }
+ return result;
+ }
+ }
+
+ public LifecycleLevel value();
+}
diff --git a/api/src/main/java/org/killbill/billing/osgi/api/LiveTrackerException.java b/api/src/main/java/org/killbill/billing/osgi/api/LiveTrackerException.java
new file mode 100644
index 0000000..e0cfb06
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/osgi/api/LiveTrackerException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.api;
+
+public class LiveTrackerException extends Exception {
+
+ public LiveTrackerException(String msg) {
+ super(msg);
+ }
+
+ public LiveTrackerException(String msg, Throwable e) {
+ super(msg, e);
+ }
+}
diff --git a/api/src/main/java/org/killbill/billing/osgi/api/OSGIService.java b/api/src/main/java/org/killbill/billing/osgi/api/OSGIService.java
new file mode 100644
index 0000000..6d17ce5
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/osgi/api/OSGIService.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.api;
+
+import org.killbill.billing.lifecycle.KillbillService;
+
+public interface OSGIService extends KillbillService {
+}
diff --git a/api/src/main/java/org/killbill/billing/osgi/api/OSGIServiceDescriptor.java b/api/src/main/java/org/killbill/billing/osgi/api/OSGIServiceDescriptor.java
new file mode 100644
index 0000000..8c8f553
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/osgi/api/OSGIServiceDescriptor.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.api;
+
+public interface OSGIServiceDescriptor {
+
+ /**
+ * @return the symbolic name of the OSGI plugin registering that service
+ */
+ public String getPluginSymbolicName();
+
+ /**
+ *
+ * @return the unique of that service-- plugin should rely on namespace to enforce the uniqueness
+ */
+ public String getRegistrationName();
+
+}
diff --git a/api/src/main/java/org/killbill/billing/osgi/api/OSGIServiceRegistration.java b/api/src/main/java/org/killbill/billing/osgi/api/OSGIServiceRegistration.java
new file mode 100644
index 0000000..9e5be04
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/osgi/api/OSGIServiceRegistration.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.api;
+
+import java.util.Set;
+
+/**
+ * The purpose is to register within Killbill OSGI services
+ * that were exported by specific Killbill plugins
+ *
+ * @param <T> The OSGI service exported by Killbill bundles
+ */
+public interface OSGIServiceRegistration<T> {
+
+ void registerService(OSGIServiceDescriptor desc, T service);
+
+ /**
+ * @param serviceName the name of the service as it was registered
+ */
+ void unregisterService(String serviceName);
+
+ /**
+ * @param serviceName the name of the service as it was registered
+ * @return the instance that was registered under that name
+ */
+ T getServiceForName(String serviceName);
+
+ /**
+ * @return the set of all the service registered
+ */
+ Set<String> getAllServices();
+
+ /**
+ * @return the type of service that is registered under that OSGIServiceRegistration
+ */
+ Class<T> getServiceType();
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/applicator/formatters/BillingStateFormatter.java b/api/src/main/java/org/killbill/billing/overdue/applicator/formatters/BillingStateFormatter.java
new file mode 100644
index 0000000..cfa2705
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/applicator/formatters/BillingStateFormatter.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.applicator.formatters;
+
+import org.killbill.billing.overdue.config.api.BillingState;
+
+public abstract class BillingStateFormatter extends BillingState {
+
+ public BillingStateFormatter(final BillingState billingState) {
+ super(billingState.getObjectId(), billingState.getNumberOfUnpaidInvoices(), billingState.getBalanceOfUnpaidInvoices(),
+ billingState.getDateOfEarliestUnpaidInvoice(), billingState.getAccountTimeZone(), billingState.getIdOfEarliestUnpaidInvoice(),
+ billingState.getResponseForLastFailedPayment(), billingState.getTags());
+ }
+
+ public abstract String getFormattedBalanceOfUnpaidInvoices();
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/applicator/formatters/OverdueEmailFormatterFactory.java b/api/src/main/java/org/killbill/billing/overdue/applicator/formatters/OverdueEmailFormatterFactory.java
new file mode 100644
index 0000000..ce3cdad
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/applicator/formatters/OverdueEmailFormatterFactory.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.applicator.formatters;
+
+import org.killbill.billing.overdue.config.api.BillingState;
+
+public interface OverdueEmailFormatterFactory {
+
+ public BillingStateFormatter createBillingStateFormatter(BillingState billingState);
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/Condition.java b/api/src/main/java/org/killbill/billing/overdue/Condition.java
new file mode 100644
index 0000000..9ec3d5a
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/Condition.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.entitlement.api.Blockable;
+import org.killbill.billing.overdue.config.api.BillingState;
+
+
+public interface Condition {
+
+ /**
+ * Evaluate the condition in a given state, at a given date.
+ *
+ * @param state the billing state
+ * @param now the day to use to evaluate the condition, in the account timezone
+ * @return true if the condition is true, false otherwise
+ */
+ public boolean evaluate(BillingState state, LocalDate now);
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/config/api/BillingState.java b/api/src/main/java/org/killbill/billing/overdue/config/api/BillingState.java
new file mode 100644
index 0000000..35b7c02
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/config/api/BillingState.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config.api;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.util.tag.Tag;
+
+public class BillingState {
+
+ private final UUID objectId;
+ private final int numberOfUnpaidInvoices;
+ private final BigDecimal balanceOfUnpaidInvoices;
+ private final LocalDate dateOfEarliestUnpaidInvoice;
+ private final DateTimeZone accountTimeZone;
+ private final UUID idOfEarliestUnpaidInvoice;
+ private final PaymentResponse responseForLastFailedPayment;
+ private final Tag[] tags;
+
+ public BillingState(final UUID id,
+ final int numberOfUnpaidInvoices,
+ final BigDecimal balanceOfUnpaidInvoices,
+ final LocalDate dateOfEarliestUnpaidInvoice,
+ final DateTimeZone accountTimeZone,
+ final UUID idOfEarliestUnpaidInvoice,
+ final PaymentResponse responseForLastFailedPayment,
+ final Tag[] tags) {
+ this.objectId = id;
+ this.numberOfUnpaidInvoices = numberOfUnpaidInvoices;
+ this.balanceOfUnpaidInvoices = balanceOfUnpaidInvoices;
+ this.dateOfEarliestUnpaidInvoice = dateOfEarliestUnpaidInvoice;
+ this.accountTimeZone = accountTimeZone;
+ this.idOfEarliestUnpaidInvoice = idOfEarliestUnpaidInvoice;
+ this.responseForLastFailedPayment = responseForLastFailedPayment;
+ this.tags = tags;
+ }
+
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ public int getNumberOfUnpaidInvoices() {
+ return numberOfUnpaidInvoices;
+ }
+
+ public BigDecimal getBalanceOfUnpaidInvoices() {
+ return balanceOfUnpaidInvoices;
+ }
+
+ public LocalDate getDateOfEarliestUnpaidInvoice() {
+ return dateOfEarliestUnpaidInvoice;
+ }
+
+ public UUID getIdOfEarliestUnpaidInvoice() {
+ return idOfEarliestUnpaidInvoice;
+ }
+
+ public PaymentResponse getResponseForLastFailedPayment() {
+ return responseForLastFailedPayment;
+ }
+
+ public Tag[] getTags() {
+ return tags;
+ }
+
+ public DateTimeZone getAccountTimeZone() {
+ return accountTimeZone;
+ }
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/config/api/OverdueException.java b/api/src/main/java/org/killbill/billing/overdue/config/api/OverdueException.java
new file mode 100644
index 0000000..e02f487
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/config/api/OverdueException.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config.api;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.ErrorCode;
+
+public class OverdueException extends BillingExceptionBase {
+
+ public OverdueException(final BillingExceptionBase cause) {
+ super(cause);
+ }
+
+ public OverdueException(final Throwable cause, final int code, final String msg) {
+ super(cause, code, msg);
+ }
+
+ private static final long serialVersionUID = 1L;
+
+ public OverdueException(final Throwable cause, final ErrorCode code, final Object... args) {
+ super(cause, code, args);
+ }
+
+ public OverdueException(final ErrorCode code, final Object... args) {
+ super(code, args);
+ }
+
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/config/api/OverdueStateSet.java b/api/src/main/java/org/killbill/billing/overdue/config/api/OverdueStateSet.java
new file mode 100644
index 0000000..5378ed3
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/config/api/OverdueStateSet.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config.api;
+
+import org.joda.time.LocalDate;
+import org.joda.time.Period;
+
+import org.killbill.billing.overdue.OverdueApiException;
+import org.killbill.billing.overdue.OverdueState;
+
+public interface OverdueStateSet {
+
+ public OverdueState getClearState() throws OverdueApiException;
+
+ public OverdueState findState(String stateName) throws OverdueApiException;
+
+ /**
+ * Compute an overdue state, given a billing state, at a given day.
+ *
+ * @param billingState the billing state
+ * @param now the day to use to calculate the overdue state, in the account timezone
+ * @return the overdue state
+ * @throws OverdueApiException
+ */
+ public OverdueState calculateOverdueState(BillingState billingState, LocalDate now) throws OverdueApiException;
+
+ public int size();
+
+ public OverdueState getFirstState();
+
+ public Period getInitialReevaluationInterval();
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/config/api/PaymentResponse.java b/api/src/main/java/org/killbill/billing/overdue/config/api/PaymentResponse.java
new file mode 100644
index 0000000..858b002
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/config/api/PaymentResponse.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config.api;
+
+public enum PaymentResponse {
+ // Card issues
+ INVALID_CARD("The card number, expiry date or cvc is invalid or incorrect"),
+ EXPIRED_CARD("The card has expired"),
+ LOST_OR_STOLEN_CARD("The card has been lost or stolen"),
+
+ // Account issues
+ DO_NOT_HONOR("Do not honor the card - usually a problem with account"),
+ INSUFFICIENT_FUNDS("The account had insufficient funds to fulfil the payment"),
+ DECLINE("Generic payment decline"),
+
+ //Transaction
+ PROCESSING_ERROR("Error processing card"),
+ INVALID_AMOUNT("An invalid amount was entered"),
+ DUPLICATE_TRANSACTION("A transaction with identical amount and credit card information was submitted very recently."),
+
+ //Other
+ OTHER("Some other error");
+
+ private final String description;
+
+ private PaymentResponse(final String description) {
+ this.description = description;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ // 690118 | Approved
+ // 136956 | Do Not Honor
+ // 119640 | Insufficient Funds
+ // 68514 | Invalid Account Number
+ // 66824 | Declined: 10417-The transaction cannot complete successfully. Instruct the customer to use an alternative payment
+ // 55473 | Declined: 10201-Agreement was canceled
+ // 30930 | Pick Up Card
+ // 29857 | Lost/Stolen Card
+ // 28197 | Declined
+ // 24830 | Declined: 10207-Transaction failed but user has alternate funding source
+ // 18445 | Generic Decline
+ // 18254 | Expired Card
+ // 16521 | Cardholder transaction not permitted
+ // 11576 | Restricted Card
+ // 7410 | Account Number Does Not Match Payment Type
+ // 7312 | Invalid merchant information: 10507-Payer's account is denied
+ // 6425 | Invalid Transaction
+ // 2825 | Declined: 10204-User's account is closed or restricted
+ // 2730 | Invalid account number
+ // 1331 |
+ // 1240 | Field format error: 10561-There's an error with this transaction. Please enter a complete billing address.
+ // 1125 | Cardholder requested that recurring or installment payment be stopped
+ // 1060 | No such issuer
+ // 1047 | Issuer Unavailable
+ // 816 | Not signed up for this tender type
+ // 749 | Transaction not allowed at terminal
+ // 663 | Invalid expiration date: 0910
+ // 548 | Invalid expiration date: 1010
+ // 542 | Invalid expiration date:
+ // 500 | Invalid expiration date: 0810
+ // 492 | Invalid expiration date: 1110
+ // 410 | Invalid expiration date: 0710
+ // 388 | Exceeds Approval Amount Limit
+ // 362 | Generic processor error: 10001-Internal Error
+ // 313 | Exceeds per transaction limit: 10553-This transaction cannot be processed.
+ // 310 | Decline CVV2/CID Fail
+ // 309 | Generic processor error: 10201-Agreement was canceled
+ // 278 | Generic processor error: 10417-The transaction cannot complete successfully. Instruct the customer to use an alte
+ // 246 | Call Issuer
+ // 237 | Generic processor error: 11091-The transaction was blocked as it would exceed the sending limit for this buyer.
+ // 202 | Failed to connect to host Input Server Uri = https://payflowpro.paypal.com:443
+ // 166 | Exceeds number of PIN entries
+ // 150 | Invalid Amount
+
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/EmailNotification.java b/api/src/main/java/org/killbill/billing/overdue/EmailNotification.java
new file mode 100644
index 0000000..2b0fd96
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/EmailNotification.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+public interface EmailNotification {
+
+ public String getSubject();
+
+ public String getTemplateName();
+
+ public Boolean isHTML();
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/OverdueApiException.java b/api/src/main/java/org/killbill/billing/overdue/OverdueApiException.java
new file mode 100644
index 0000000..afc22b6
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/OverdueApiException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.ErrorCode;
+
+public class OverdueApiException extends BillingExceptionBase {
+
+ private static final long serialVersionUID = 1L;
+
+ public OverdueApiException(BillingExceptionBase o) {
+ super(o);
+ }
+
+ public OverdueApiException(final Throwable cause, final ErrorCode code, final Object... args) {
+ super(cause, code, args);
+ }
+
+ public OverdueApiException(final ErrorCode code, final Object... args) {
+ super(code, args);
+ }
+
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/OverdueCancellationPolicy.java b/api/src/main/java/org/killbill/billing/overdue/OverdueCancellationPolicy.java
new file mode 100644
index 0000000..b74809c
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/OverdueCancellationPolicy.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+public enum OverdueCancellationPolicy {
+ END_OF_TERM,
+ IMMEDIATE,
+ NONE
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/OverdueService.java b/api/src/main/java/org/killbill/billing/overdue/OverdueService.java
new file mode 100644
index 0000000..ef8267b
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/OverdueService.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+import org.killbill.billing.lifecycle.KillbillService;
+
+public interface OverdueService extends KillbillService {
+ String OVERDUE_SERVICE_NAME = "overdue-service";
+
+ public String getName();
+
+ public OverdueUserApi getUserApi();
+
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/OverdueState.java b/api/src/main/java/org/killbill/billing/overdue/OverdueState.java
new file mode 100644
index 0000000..99fb85a
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/OverdueState.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+import org.joda.time.Period;
+
+
+public interface OverdueState {
+
+ public String getName();
+
+ public String getExternalMessage();
+
+ public int getDaysBetweenPaymentRetries();
+
+ public boolean disableEntitlementAndChangesBlocked();
+
+ public OverdueCancellationPolicy getSubscriptionCancellationPolicy();
+
+ public boolean blockChanges();
+
+ public boolean isClearState();
+
+ public Period getReevaluationInterval() throws OverdueApiException;
+
+ public Condition getCondition();
+
+ public EmailNotification getEnterStateEmailNotification();
+}
diff --git a/api/src/main/java/org/killbill/billing/overdue/OverdueUserApi.java b/api/src/main/java/org/killbill/billing/overdue/OverdueUserApi.java
new file mode 100644
index 0000000..4d943d7
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/overdue/OverdueUserApi.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.overdue.config.api.BillingState;
+import org.killbill.billing.overdue.config.api.OverdueException;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+public interface OverdueUserApi {
+
+ public OverdueState refreshOverdueStateFor(Account overdueable, CallContext context) throws OverdueException, OverdueApiException;
+
+ public void setOverrideBillingStateForAccount(Account overdueable, BillingState state, CallContext context) throws OverdueException;
+
+ public OverdueState getOverdueStateFor(Account overdueable, TenantContext context) throws OverdueException;
+
+ public BillingState getBillingStateFor(Account overdueable, TenantContext context) throws OverdueException;
+
+}
diff --git a/api/src/main/java/org/killbill/billing/payment/api/PaymentInternalApi.java b/api/src/main/java/org/killbill/billing/payment/api/PaymentInternalApi.java
new file mode 100644
index 0000000..a8b0c5d
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/payment/api/PaymentInternalApi.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentMethod;
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+public interface PaymentInternalApi {
+
+ public Payment getPayment(UUID paymentId, InternalTenantContext context)
+ throws PaymentApiException;
+
+ public PaymentMethod getPaymentMethodById(UUID paymentMethodId, final boolean includedInactive, InternalTenantContext context)
+ throws PaymentApiException;
+
+ public List<Payment> getAccountPayments(UUID accountId, InternalTenantContext context)
+ throws PaymentApiException;
+
+ public List<PaymentMethod> getPaymentMethods(Account account, InternalTenantContext context)
+ throws PaymentApiException;
+}
diff --git a/api/src/main/java/org/killbill/billing/payment/api/PaymentService.java b/api/src/main/java/org/killbill/billing/payment/api/PaymentService.java
new file mode 100644
index 0000000..1d2b916
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/payment/api/PaymentService.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import org.killbill.billing.lifecycle.KillbillService;
+
+public interface PaymentService extends KillbillService {
+ @Override
+ String getName();
+
+ PaymentApi getPaymentApi();
+
+}
diff --git a/api/src/main/java/org/killbill/billing/payment/plugin/api/NoOpPaymentPluginApi.java b/api/src/main/java/org/killbill/billing/payment/plugin/api/NoOpPaymentPluginApi.java
new file mode 100644
index 0000000..bab9f06
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/payment/plugin/api/NoOpPaymentPluginApi.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.plugin.api;
+
+public interface NoOpPaymentPluginApi extends PaymentPluginApi {
+
+ public void clear();
+
+ public void makeNextPaymentFailWithError();
+
+ public void makeNextPaymentFailWithException();
+
+ public void makeAllInvoicesFailWithError(boolean failure);
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/migration/SubscriptionBaseMigrationApi.java b/api/src/main/java/org/killbill/billing/subscription/api/migration/SubscriptionBaseMigrationApi.java
new file mode 100644
index 0000000..a265769
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/migration/SubscriptionBaseMigrationApi.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.migration;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.util.callcontext.CallContext;
+
+/**
+ * The interface {@code SubscriptionBaseMigrationApi} is used to migrate subscription data from third party system
+ * in an atomic way.
+ */
+public interface SubscriptionBaseMigrationApi {
+
+
+ /**
+ * The interface {@code AccountMigration} captures all the {@code SubscriptionBaseBundle} associated with
+ * that account.
+ */
+ public interface AccountMigration {
+
+ /**
+ *
+ * @return the unique id for the account
+ */
+ public UUID getAccountKey();
+
+ /**
+ *
+ * @return an array of {@code BundleMigration}
+ */
+ public BundleMigration[] getBundles();
+ }
+
+ /**
+ * The interface {@code BundleMigration} captures all the {@code SubscriptionBase} asociated with a given
+ * {@code SubscriptionBaseBundle}
+ */
+ public interface BundleMigration {
+
+ /**
+ *
+ * @return the bundle external key
+ */
+ public String getBundleKey();
+
+ /**
+ *
+ * @return an array of {@code SubscriptionBase}
+ */
+ public SubscriptionMigration[] getSubscriptions();
+ }
+
+ /**
+ * The interface {@code SubscriptionMigration} captures the detail for each {@code SubscriptionBase} to be
+ * migrated.
+ */
+ public interface SubscriptionMigration {
+
+ /**
+ *
+ * @return the {@code ProductCategory}
+ */
+ public ProductCategory getCategory();
+
+ /**
+ *
+ * @return the chargeTroughDate for that {@code SubscriptionBase}
+ */
+ public DateTime getChargedThroughDate();
+
+ /**
+ *
+ * @return the various phase information for that {@code SubscriptionBase}
+ */
+ public SubscriptionMigrationCase[] getSubscriptionCases();
+ }
+
+ /**
+ * The interface {@code SubscriptionMigrationCase} captures the details of
+ * phase for a {@code SubscriptionBase}.
+ *
+ */
+ public interface SubscriptionMigrationCase {
+ /**
+ *
+ * @return the {@code PlanPhaseSpecifier}
+ */
+ public PlanPhaseSpecifier getPlanPhaseSpecifier();
+
+ /**
+ *
+ * @return the date at which this phase starts.
+ */
+ public DateTime getEffectiveDate();
+
+ /**
+ *
+ * @return the date at which this phase is stopped.
+ */
+ public DateTime getCancelledDate();
+ }
+
+
+ /**
+ * Migrate all the existing entitlements associated with that account.
+ * The semantics is 'all or nothing' (atomic operation)
+ *
+ * @param toBeMigrated all the bundles and associated SubscriptionBase that should be migrated for the account
+ * @throws SubscriptionBaseMigrationApiException
+ * an subscription api exception
+ */
+ public void migrate(AccountMigration toBeMigrated, CallContext context)
+ throws SubscriptionBaseMigrationApiException;
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/migration/SubscriptionBaseMigrationApiException.java b/api/src/main/java/org/killbill/billing/subscription/api/migration/SubscriptionBaseMigrationApiException.java
new file mode 100644
index 0000000..dfa5de5
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/migration/SubscriptionBaseMigrationApiException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.migration;
+
+public class SubscriptionBaseMigrationApiException extends Exception {
+
+ private static final long serialVersionUID = 7623133L;
+
+ public SubscriptionBaseMigrationApiException() {
+ super();
+ }
+
+ public SubscriptionBaseMigrationApiException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public SubscriptionBaseMigrationApiException(final String message) {
+ super(message);
+ }
+
+ public SubscriptionBaseMigrationApiException(final Throwable cause) {
+ super(cause);
+ }
+}
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
new file mode 100644
index 0000000..fca316c
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+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.PriceList;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Blockable;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementSourceType;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.entity.Entity;
+
+public interface SubscriptionBase extends Entity, Blockable {
+
+ public boolean cancel(final CallContext context)
+ throws SubscriptionBaseApiException;
+
+ public boolean cancelWithDate(final DateTime requestedDate, final CallContext context)
+ throws SubscriptionBaseApiException;
+
+ public boolean cancelWithPolicy(final BillingActionPolicy policy, final CallContext context)
+ throws SubscriptionBaseApiException;
+
+ public boolean uncancel(final CallContext context)
+ throws SubscriptionBaseApiException;
+
+ // Return the effective date of the change
+ public DateTime changePlan(final String productName, final BillingPeriod term, final String priceList, final CallContext context)
+ throws SubscriptionBaseApiException;
+
+ // Return the effective date of the change
+ public DateTime changePlanWithDate(final String productName, final BillingPeriod term, final String priceList, final DateTime requestedDate, final CallContext context)
+ throws SubscriptionBaseApiException;
+
+ // Return the effective date of the change
+ public DateTime changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+ final BillingActionPolicy policy, final CallContext context)
+ throws SubscriptionBaseApiException;
+
+ public UUID getBundleId();
+
+ public EntitlementState getState();
+
+ public EntitlementSourceType getSourceType();
+
+ public DateTime getStartDate();
+
+ public DateTime getEndDate();
+
+ public DateTime getFutureEndDate();
+
+ public Plan getCurrentPlan();
+
+ public Plan getLastActivePlan();
+
+ public PlanPhase getLastActivePhase();
+
+ public PriceList getCurrentPriceList();
+
+ public PlanPhase getCurrentPhase();
+
+ public Product getLastActiveProduct();
+
+ public PriceList getLastActivePriceList();
+
+ public ProductCategory getLastActiveCategory();
+
+ public BillingPeriod getLastActiveBillingPeriod();
+
+ public DateTime getChargedThroughDate();
+
+ public ProductCategory getCategory();
+
+ public SubscriptionBaseTransition getPendingTransition();
+
+ public SubscriptionBaseTransition getPreviousTransition();
+
+ public List<SubscriptionBaseTransition> getAllTransitions();
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseService.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseService.java
new file mode 100644
index 0000000..106dd63
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseService.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api;
+
+import org.killbill.billing.lifecycle.KillbillService;
+
+/**
+ * The interface {@code SubscriptionBaseService} is a {@code KillbillService} required to handle subscription operations
+ */
+public interface SubscriptionBaseService extends KillbillService {
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
new file mode 100644
index 0000000..11318ce
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api;
+
+/**
+ * The {@code SubscriptionBaseTransitionType}
+ */
+public enum SubscriptionBaseTransitionType {
+ /**
+ * Occurs when a {@code SubscriptionBase} got migrated to mark the start of the subscription
+ */
+ MIGRATE_ENTITLEMENT,
+ /**
+ * Occurs when a a user created a {@code SubscriptionBase} (not migrated)
+ */
+ CREATE,
+ /**
+ * Occurs when a {@code SubscriptionBase} got migrated to mark the start of the billing
+ */
+ MIGRATE_BILLING,
+ /**
+ * Occurs when a {@code SubscriptionBase} got transferred to mark the start of the subscription
+ */
+ TRANSFER,
+ /**
+ * Occurs when a user changed the current {@code Plan} of the {@code SubscriptionBase}
+ */
+ CHANGE,
+ /**
+ * Occurs when a user restarted a {@code SubscriptionBase} after it had been cancelled
+ */
+ RE_CREATE,
+ /**
+ * Occurs when a user cancelled the {@code SubscriptionBase}
+ */
+ CANCEL,
+ /**
+ * Occurs when a user uncancelled the {@code SubscriptionBase} before it reached its cancellation date
+ */
+ UNCANCEL,
+ /**
+ * Generated by the system to mark a change of phase
+ */
+ PHASE,
+ /**
+ * Generated by the system to mark the start of blocked billing overdue state. This is not on disk but computed by junction to create the billing events.
+ */
+ START_BILLING_DISABLED,
+ /**
+ * Generated by the system to mark the end of blocked billing overdue state. This is not on disk but computed by junction to create the billing events.
+ */
+ END_BILLING_DISABLED
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBillingApiException.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBillingApiException.java
new file mode 100644
index 0000000..7b0d7e1
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBillingApiException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.ErrorCode;
+
+public class SubscriptionBillingApiException extends BillingExceptionBase {
+ private static final long serialVersionUID = 127392038L;
+
+ public SubscriptionBillingApiException(final Throwable cause, final int code, final String msg) {
+ super(cause, code, msg);
+ }
+
+ public SubscriptionBillingApiException(final Throwable cause, final ErrorCode code, final Object... args) {
+ super(cause, code, args);
+ }
+
+ public SubscriptionBillingApiException(final ErrorCode code, final Object... args) {
+ super(code, args);
+ }
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/timeline/BundleBaseTimeline.java b/api/src/main/java/org/killbill/billing/subscription/api/timeline/BundleBaseTimeline.java
new file mode 100644
index 0000000..85c1d2a
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/timeline/BundleBaseTimeline.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.util.entity.Entity;
+
+/**
+ * The interface {@code BundleBaseTimeline} shows a view of all the subscription events for a specific
+ * {@code SubscriptionBaseBundle}.
+ */
+public interface BundleBaseTimeline extends Entity {
+
+ /**
+ * @return a unique viewId to identify whether two calls who display the same view or a different view
+ */
+ String getViewId();
+
+ /**
+ * @return the unique id for the {@SubscriptionBundle}
+ */
+ UUID getId();
+
+ /**
+ * @return the external Key for the {@SubscriptionBundle}
+ */
+ String getExternalKey();
+
+ /**
+ * @return the list of {@code SubscriptionBaseTimeline}
+ */
+ List<SubscriptionBaseTimeline> getSubscriptions();
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseRepairException.java b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseRepairException.java
new file mode 100644
index 0000000..a561826
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseRepairException.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+
+public class SubscriptionBaseRepairException extends BillingExceptionBase {
+
+ private static final long serialVersionUID = 19067233L;
+
+ public SubscriptionBaseRepairException(final SubscriptionBaseApiException e) {
+ super(e, e.getCode(), e.getMessage());
+ }
+
+ public SubscriptionBaseRepairException(final CatalogApiException e) {
+ super(e, e.getCode(), e.getMessage());
+ }
+
+ public SubscriptionBaseRepairException(final Throwable e, final ErrorCode code, final Object... args) {
+ super(e, code, args);
+ }
+
+ public SubscriptionBaseRepairException(final ErrorCode code, final Object... args) {
+ super(code, args);
+ }
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
new file mode 100644
index 0000000..c8a3ad6
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimeline.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.util.entity.Entity;
+
+/**
+ * The interface {@code} shows a view of all the events for a particular {@code SubscriptionBase}.
+ * <p/>
+ * It can be used to display information, or it can be used to modify the subscription stream of events
+ * and 'repair' the stream by versioning the events.
+ */
+public interface SubscriptionBaseTimeline extends Entity {
+
+ /**
+ * @return the list of events that should be deleted when repairing the stream.
+ */
+ public List<DeletedEvent> getDeletedEvents();
+
+ /**
+ * @return the list of events that should be added when repairing the stream
+ */
+ public List<NewEvent> getNewEvents();
+
+ /**
+ * @return the current list of events for that {@code SubscriptionBase}
+ */
+ public List<ExistingEvent> getExistingEvents();
+
+ /**
+ * @return the active version for the event stream
+ */
+ public long getActiveVersion();
+
+
+ public interface DeletedEvent {
+
+ /**
+ * @return the unique if for the event to delete
+ */
+ public UUID getEventId();
+ }
+
+ public interface NewEvent {
+
+ /**
+ * @return the description for the event to be added
+ */
+ public PlanPhaseSpecifier getPlanPhaseSpecifier();
+
+ /**
+ * @return the date at which this event should be inserted into the stream
+ */
+ public DateTime getRequestedDate();
+
+ /**
+ * @return the {@code SubscriptionBaseTransitionType} for the event
+ */
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType();
+
+ }
+
+ public interface ExistingEvent extends DeletedEvent, NewEvent {
+
+ /**
+ * @return the date at which this event was effective
+ */
+ public DateTime getEffectiveDate();
+
+ /**
+ * @return the name of the phase
+ */
+ public String getPlanPhaseName();
+ }
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimelineApi.java b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimelineApi.java
new file mode 100644
index 0000000..ae0a861
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionBaseTimelineApi.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.UUID;
+
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+public interface SubscriptionBaseTimelineApi {
+
+ public BundleBaseTimeline getBundleTimeline(SubscriptionBaseBundle bundle, TenantContext context)
+ throws SubscriptionBaseRepairException;
+
+ public BundleBaseTimeline getBundleTimeline(UUID accountId, String bundleName, TenantContext context)
+ throws SubscriptionBaseRepairException;
+
+ public BundleBaseTimeline getBundleTimeline(UUID bundleId, TenantContext context)
+ throws SubscriptionBaseRepairException;
+
+ public BundleBaseTimeline repairBundle(BundleBaseTimeline input, boolean dryRun, CallContext context)
+ throws SubscriptionBaseRepairException;
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/transfer/SubscriptionBaseTransferApi.java b/api/src/main/java/org/killbill/billing/subscription/api/transfer/SubscriptionBaseTransferApi.java
new file mode 100644
index 0000000..416b061
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/transfer/SubscriptionBaseTransferApi.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.transfer;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.util.callcontext.CallContext;
+
+/**
+ * The interface {@code SubscriptionBaseTransferApi} is used to transfer a bundle from one account to another account.
+ */
+public interface SubscriptionBaseTransferApi {
+
+ /**
+ * @param sourceAccountId the unique id for the account on which the bundle will be transferred from
+ * @param destAccountId the unique id for the account on which the bundle will be transferred to
+ * @param bundleKey the externalKey for the bundle
+ * @param requestedDate the date at which this transfer should occur
+ * @param transferAddOn whether or not we should also transfer ADD_ON subscriptions existing on that {@code SubscriptionBaseBundle}
+ * @param cancelImmediately whether cancellation on the sourceAccount occurs immediately
+ * @param context the user callcontext
+ * @return the newly created {@code SubscriptionBaseBundle}
+ * @throws SubscriptionBaseTransferApiException
+ * if the system could not transfer the {@code SubscriptionBaseBundle}
+ */
+ public SubscriptionBaseBundle transferBundle(final UUID sourceAccountId, final UUID destAccountId, final String bundleKey, final DateTime requestedDate,
+ final boolean transferAddOn, final boolean cancelImmediately, final CallContext context)
+ throws SubscriptionBaseTransferApiException;
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/transfer/SubscriptionBaseTransferApiException.java b/api/src/main/java/org/killbill/billing/subscription/api/transfer/SubscriptionBaseTransferApiException.java
new file mode 100644
index 0000000..a88eabe
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/transfer/SubscriptionBaseTransferApiException.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.transfer;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseRepairException;
+
+public class SubscriptionBaseTransferApiException extends BillingExceptionBase {
+
+ private static final long serialVersionUID = 17086131L;
+
+ public SubscriptionBaseTransferApiException(final CatalogApiException e) {
+ super(e, e.getCode(), e.getMessage());
+ }
+
+ public SubscriptionBaseTransferApiException(final SubscriptionBaseRepairException e) {
+ super(e, e.getCode(), e.getMessage());
+ }
+
+ public SubscriptionBaseTransferApiException(final Throwable e, final ErrorCode code, final Object... args) {
+ super(e, code, args);
+ }
+
+ public SubscriptionBaseTransferApiException(final Throwable e, final int code, final String message) {
+ super(e, code, message);
+ }
+
+ public SubscriptionBaseTransferApiException(final ErrorCode code, final Object... args) {
+ super(code, args);
+ }
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseApiException.java b/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseApiException.java
new file mode 100644
index 0000000..99e98b9
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseApiException.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.CatalogApiException;
+
+public class SubscriptionBaseApiException extends BillingExceptionBase {
+
+ private static final long serialVersionUID = 19083233L;
+
+ public SubscriptionBaseApiException(final CatalogApiException e) {
+ super(e, e.getCode(), e.getMessage());
+ }
+
+ public SubscriptionBaseApiException(final Throwable e, final ErrorCode code, final Object... args) {
+ super(e, code, args);
+ }
+
+ public SubscriptionBaseApiException(final Throwable e, final int code, final String message) {
+ super(e, code, message);
+ }
+
+ public SubscriptionBaseApiException(final ErrorCode code, final Object... args) {
+ super(code, args);
+ }
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseBundle.java b/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseBundle.java
new file mode 100644
index 0000000..db81f92
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseBundle.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.entitlement.api.Blockable;
+import org.killbill.billing.overdue.OverdueState;
+import org.killbill.billing.util.entity.Entity;
+
+public interface SubscriptionBaseBundle extends Blockable, Entity {
+
+ public UUID getAccountId();
+
+ public String getExternalKey();
+
+ public DateTime getOriginalCreatedDate();
+}
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransition.java b/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransition.java
new file mode 100644
index 0000000..5ab44a4
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransition.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+
+public interface SubscriptionBaseTransition {
+
+ public UUID getId();
+
+ public UUID getSubscriptionId();
+
+ public UUID getBundleId();
+
+ public EntitlementState getPreviousState();
+
+ public EntitlementState getNextState();
+
+ public UUID getPreviousEventId();
+
+ public DateTime getPreviousEventCreatedDate();
+
+ public Plan getPreviousPlan();
+
+ public Plan getNextPlan();
+
+ public PlanPhase getPreviousPhase();
+
+ public UUID getNextEventId();
+
+ public DateTime getNextEventCreatedDate();
+
+ public PlanPhase getNextPhase();
+
+ public PriceList getPreviousPriceList();
+
+ public PriceList getNextPriceList();
+
+ public DateTime getRequestedTransitionTime();
+
+ public DateTime getEffectiveTransitionTime();
+
+ public SubscriptionBaseTransitionType getTransitionType();
+
+ public DateTime getCreatedDate();
+}
diff --git a/api/src/main/java/org/killbill/billing/tag/TagInternalApi.java b/api/src/main/java/org/killbill/billing/tag/TagInternalApi.java
new file mode 100644
index 0000000..7181a4f
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/tag/TagInternalApi.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tag;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.tag.Tag;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public interface TagInternalApi {
+
+ public List<TagDefinition> getTagDefinitions(InternalTenantContext context);
+
+ /**
+ * Return tags for a given object
+ *
+ * @param objectId the object id
+ * @param objectType the object type
+ * @param context call callcontext
+ * @return mapping tag id -> tag
+ */
+ public List<Tag> getTags(UUID objectId, ObjectType objectType, InternalTenantContext context);
+
+ public void addTag(final UUID objectId, final ObjectType objectType, UUID tagDefinitionId, InternalCallContext context) throws TagApiException;
+
+ public void removeTag(final UUID objectId, final ObjectType objectType, final UUID tagDefinitionId, InternalCallContext context) throws TagApiException;
+}
diff --git a/api/src/main/java/org/killbill/billing/tenant/api/TenantService.java b/api/src/main/java/org/killbill/billing/tenant/api/TenantService.java
new file mode 100644
index 0000000..8f484c2
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/tenant/api/TenantService.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.api;
+
+import org.killbill.billing.lifecycle.KillbillService;
+
+public interface TenantService extends KillbillService {
+
+}
diff --git a/api/src/main/java/org/killbill/billing/util/email/EmailApiException.java b/api/src/main/java/org/killbill/billing/util/email/EmailApiException.java
new file mode 100644
index 0000000..a03e95c
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/util/email/EmailApiException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.email;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.ErrorCode;
+
+public class EmailApiException extends BillingExceptionBase {
+ private static final long serialVersionUID = 1L;
+
+ public EmailApiException(final Throwable cause, final int code, final String msg) {
+ super(cause, code, msg);
+ }
+
+ public EmailApiException(final Throwable cause, final ErrorCode code, final Object... args) {
+ super(cause, code, args);
+ }
+
+ public EmailApiException(final ErrorCode code, final Object... args) {
+ super(code, args);
+ }
+}
diff --git a/api/src/main/java/org/killbill/billing/util/email/EmailSender.java b/api/src/main/java/org/killbill/billing/util/email/EmailSender.java
new file mode 100644
index 0000000..97067a1
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/util/email/EmailSender.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.email;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface EmailSender {
+
+ public void sendHTMLEmail(List<String> to, List<String> cc, String subject, String htmlBody) throws IOException, EmailApiException;
+
+ public void sendPlainTextEmail(List<String> to, List<String> cc, String subject, String body) throws IOException, EmailApiException;
+}
diff --git a/api/src/main/java/org/killbill/billing/util/template/translation/Translator.java b/api/src/main/java/org/killbill/billing/util/template/translation/Translator.java
new file mode 100644
index 0000000..015cc26
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/util/template/translation/Translator.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.template.translation;
+
+import java.util.Locale;
+
+public interface Translator {
+ public String getTranslation(Locale locale, String originalText);
+}
diff --git a/api/src/main/java/org/killbill/billing/util/template/translation/TranslatorConfig.java b/api/src/main/java/org/killbill/billing/util/template/translation/TranslatorConfig.java
new file mode 100644
index 0000000..f6246d6
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/util/template/translation/TranslatorConfig.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.template.translation;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
+
+public interface TranslatorConfig {
+
+ // Common
+
+ @Config("org.killbill.default.locale")
+ @Default("en_US")
+ @Description("Default Killbill locale")
+ public String getDefaultLocale();
+
+ // Catalog
+
+ @Config("org.killbill.catalog.bundlePath")
+ @Default("org/killbill/billing/util/template/translation/CatalogTranslation")
+ @Description("Path to the catalog translation bundle")
+ String getCatalogBundlePath();
+
+ // Invoices
+ @Config("org.killbill.template.bundlePath")
+ @Default("org/killbill/billing/util/template/translation/InvoiceTranslation")
+ @Description("Path to the invoice template translation bundle")
+ public String getInvoiceTemplateBundlePath();
+
+ @Config("org.killbill.template.name")
+ @Default("org/killbill/billing/util/email/templates/HtmlInvoiceTemplate.mustache")
+ @Description("Path to the HTML invoice template")
+ String getTemplateName();
+
+ @Config("org.killbill.manualPayTemplate.name")
+ @Default("org/killbill/billing/util/email/templates/HtmlInvoiceTemplate.mustache")
+ @Description("Path to the invoice template for accounts with MANUAL_PAY tag")
+ String getManualPayTemplateName();
+
+ @Config("org.killbill.template.invoiceFormatterFactoryClass")
+ @Default("org.killbill.billing.invoice.template.formatters.DefaultInvoiceFormatterFactory")
+ @Description("Invoice formatter class")
+ Class<? extends InvoiceFormatterFactory> getInvoiceFormatterFactoryClass();
+}
beatrix/pom.xml 126(+55 -71)
diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index 44b3a14..70e36ca 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-beatrix</artifactId>
@@ -36,11 +36,6 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
@@ -50,163 +45,152 @@
<artifactId>bonecp</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-compress</artifactId>
+ <version>1.5</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-account</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-account</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-currency</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-entitlement</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-invoice</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-invoice</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-junction</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-junction</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-osgi</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-osgi-bundles-jruby</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-osgi-bundles-test-beatrix</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-osgi-bundles-test-payment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-overdue</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-payment</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-payment</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-subscription</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-tenant</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-usage</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-embeddeddb-common</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
- </dependency>
- <dependency>
- <groupId>javax.servlet</groupId>
- <artifactId>javax.servlet-api</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-compress</artifactId>
- <version>1.5</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
@@ -240,9 +224,9 @@
</goals>
<configuration>
<tasks>
- <copy file="${basedir}/../osgi-bundles/tests/beatrix/target/killbill-osgi-bundles-test-beatrix-${project.version}-jar-with-dependencies.jar" tofile="${basedir}/src/test/resources/killbill-osgi-bundles-test-beatrix-${project.version}-jar-with-dependencies.jar"></copy>
- <copy file="${basedir}/../osgi-bundles/tests/payment/target/killbill-osgi-bundles-test-payment-${project.version}-jar-with-dependencies.jar" tofile="${basedir}/src/test/resources/killbill-osgi-bundles-test-payment-${project.version}-jar-with-dependencies.jar"></copy>
- <copy file="${basedir}/../osgi-bundles/bundles/jruby/target/killbill-osgi-bundles-jruby-${project.version}.jar" tofile="${basedir}/src/test/resources/killbill-osgi-bundles-jruby-${project.version}.jar"></copy>
+ <copy file="${basedir}/../osgi-bundles/tests/beatrix/target/killbill-osgi-bundles-test-beatrix-${project.version}-jar-with-dependencies.jar" tofile="${basedir}/src/test/resources/killbill-osgi-bundles-test-beatrix-${project.version}-jar-with-dependencies.jar" />
+ <copy file="${basedir}/../osgi-bundles/tests/payment/target/killbill-osgi-bundles-test-payment-${project.version}-jar-with-dependencies.jar" tofile="${basedir}/src/test/resources/killbill-osgi-bundles-test-payment-${project.version}-jar-with-dependencies.jar" />
+ <copy file="${basedir}/../osgi-bundles/bundles/jruby/target/killbill-osgi-bundles-jruby-${project.version}.jar" tofile="${basedir}/src/test/resources/killbill-osgi-bundles-jruby-${project.version}.jar" />
</tasks>
</configuration>
</execution>
diff --git a/beatrix/src/main/java/org/killbill/billing/beatrix/DefaultBeatrixService.java b/beatrix/src/main/java/org/killbill/billing/beatrix/DefaultBeatrixService.java
new file mode 100644
index 0000000..5f27e34
--- /dev/null
+++ b/beatrix/src/main/java/org/killbill/billing/beatrix/DefaultBeatrixService.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.killbill.billing.beatrix.bus.api.BeatrixService;
+import org.killbill.billing.beatrix.extbus.BeatrixListener;
+import org.killbill.billing.beatrix.glue.BeatrixModule;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.lifecycle.LifecycleHandlerType;
+import org.killbill.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+
+public class DefaultBeatrixService implements BeatrixService {
+
+ public static final String BEATRIX_SERVICE_NAME = "beatrix-service";
+
+ private final BeatrixListener beatrixListener;
+ private final PersistentBus eventBus;
+ private final PersistentBus externalBus;
+
+ @Inject
+ public DefaultBeatrixService(final PersistentBus eventBus, @Named(BeatrixModule.EXTERNAL_BUS) final PersistentBus externalBus, final BeatrixListener beatrixListener) {
+ this.eventBus = eventBus;
+ this.externalBus = externalBus;
+ this.beatrixListener = beatrixListener;
+ }
+
+ @Override
+ public String getName() {
+ return BEATRIX_SERVICE_NAME;
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.INIT_SERVICE)
+ public void registerForNotifications() {
+ try {
+ eventBus.register(beatrixListener);
+ } catch (PersistentBus.EventBusException e) {
+ throw new RuntimeException("Unable to register to the EventBus!", e);
+ }
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE)
+ public void unregisterForNotifications() {
+ try {
+ eventBus.unregister(beatrixListener);
+ } catch (PersistentBus.EventBusException e) {
+ throw new RuntimeException("Unable to unregister to the EventBus!", e);
+ }
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.INIT_BUS)
+ public void startBus() {
+ externalBus.start();
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_BUS)
+ public void stopBus() {
+ externalBus.stop();
+ }
+}
diff --git a/beatrix/src/main/java/org/killbill/billing/beatrix/glue/BeatrixModule.java b/beatrix/src/main/java/org/killbill/billing/beatrix/glue/BeatrixModule.java
new file mode 100644
index 0000000..07be9d5
--- /dev/null
+++ b/beatrix/src/main/java/org/killbill/billing/beatrix/glue/BeatrixModule.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.beatrix.DefaultBeatrixService;
+import org.killbill.billing.beatrix.bus.api.BeatrixService;
+import org.killbill.billing.beatrix.extbus.BeatrixListener;
+import org.killbill.billing.beatrix.lifecycle.DefaultLifecycle;
+import org.killbill.billing.beatrix.lifecycle.Lifecycle;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.bus.api.PersistentBusConfig;
+import org.killbill.billing.util.glue.BusProvider;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Key;
+import com.google.inject.name.Names;
+
+public class BeatrixModule extends AbstractModule {
+
+ public static final String EXTERNAL_BUS = "externalBus";
+
+ private final ConfigSource configSource;
+
+ public BeatrixModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ @Override
+ protected void configure() {
+ installLifecycle();
+ installExternalBus();
+ }
+
+ protected void installLifecycle() {
+ bind(Lifecycle.class).to(DefaultLifecycle.class).asEagerSingleton();
+ }
+
+ protected void installExternalBus() {
+ bind(BeatrixService.class).to(DefaultBeatrixService.class);
+ bind(DefaultBeatrixService.class).asEagerSingleton();
+
+ final PersistentBusConfig extBusConfig = new ExternalPersistentBusConfig(configSource);
+
+ bind(BusProvider.class).annotatedWith(Names.named(EXTERNAL_BUS)).toInstance(new BusProvider(extBusConfig));
+ bind(PersistentBus.class).annotatedWith(Names.named(EXTERNAL_BUS)).toProvider(Key.get(BusProvider.class, Names.named(EXTERNAL_BUS))).asEagerSingleton();
+
+ bind(BeatrixListener.class).asEagerSingleton();
+ }
+}
diff --git a/beatrix/src/main/java/org/killbill/billing/beatrix/lifecycle/Lifecycle.java b/beatrix/src/main/java/org/killbill/billing/beatrix/lifecycle/Lifecycle.java
new file mode 100644
index 0000000..029bca1
--- /dev/null
+++ b/beatrix/src/main/java/org/killbill/billing/beatrix/lifecycle/Lifecycle.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.lifecycle;
+
+public interface Lifecycle {
+
+ public void fireStartupSequencePriorEventRegistration();
+
+ public void fireStartupSequencePostEventRegistration();
+
+ public void fireShutdownSequencePriorEventUnRegistration();
+
+ public void fireShutdownSequencePostEventUnRegistration();
+}
diff --git a/beatrix/src/main/resources/org/killbill/billing/beatrix/ddl.sql b/beatrix/src/main/resources/org/killbill/billing/beatrix/ddl.sql
new file mode 100644
index 0000000..2783fc6
--- /dev/null
+++ b/beatrix/src/main/resources/org/killbill/billing/beatrix/ddl.sql
@@ -0,0 +1,37 @@
+/*! SET storage_engine=INNODB */;
+
+DROP TABLE IF EXISTS bus_ext_events;
+CREATE TABLE bus_ext_events (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ class_name varchar(128) NOT NULL,
+ event_json varchar(2048) NOT NULL,
+ user_token char(36),
+ created_date datetime NOT NULL,
+ creating_owner char(50) NOT NULL,
+ processing_owner char(50) DEFAULT NULL,
+ processing_available_date datetime DEFAULT NULL,
+ processing_state varchar(14) DEFAULT 'AVAILABLE',
+ error_count int(11) unsigned DEFAULT 0,
+ search_key1 int(11) unsigned default null,
+ search_key2 int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX `idx_bus_ext_where` ON bus_ext_events (`processing_state`,`processing_owner`,`processing_available_date`);
+CREATE INDEX bus_ext_events_tenant_account_record_id ON bus_ext_events(search_key2, search_key1);
+
+DROP TABLE IF EXISTS bus_ext_events_history;
+CREATE TABLE bus_ext_events_history (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ class_name varchar(128) NOT NULL,
+ event_json varchar(2048) NOT NULL,
+ user_token char(36),
+ created_date datetime NOT NULL,
+ creating_owner char(50) NOT NULL,
+ processing_owner char(50) DEFAULT NULL,
+ processing_available_date datetime DEFAULT NULL,
+ processing_state varchar(14) DEFAULT 'AVAILABLE',
+ error_count int(11) unsigned DEFAULT 0,
+ search_key1 int(11) unsigned default null,
+ search_key2 int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/BeatrixTestSuite.java b/beatrix/src/test/java/org/killbill/billing/beatrix/BeatrixTestSuite.java
new file mode 100644
index 0000000..23eaf83
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/BeatrixTestSuite.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix;
+
+import org.killbill.billing.GuicyKillbillTestSuite;
+
+public abstract class BeatrixTestSuite extends GuicyKillbillTestSuite {
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/BeatrixTestSuiteWithEmbeddedDB.java b/beatrix/src/test/java/org/killbill/billing/beatrix/BeatrixTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..10eda00
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/BeatrixTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix;
+
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+
+public abstract class BeatrixTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java b/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java
new file mode 100644
index 0000000..b4c1cf0
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/extbus/TestEventJson.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.extbus;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.beatrix.BeatrixTestSuite;
+import org.killbill.billing.notification.plugin.api.ExtBusEventType;
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+public class TestEventJson extends BeatrixTestSuite {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Test(groups = "fast")
+ public void testBusExternalEvent() throws Exception {
+ final UUID objectId = UUID.randomUUID();
+ final UUID userToken = UUID.randomUUID();
+ final UUID accountId = UUID.randomUUID();
+ final UUID tenantId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT;
+ final ExtBusEventType extBusEventType = ExtBusEventType.ACCOUNT_CREATION;
+
+ final DefaultBusExternalEvent e = new DefaultBusExternalEvent(objectId, objectType, extBusEventType, accountId, tenantId, 1L, 2L, UUID.randomUUID());
+ final String json = mapper.writeValueAsString(e);
+
+ final Class<?> claz = Class.forName(DefaultBusExternalEvent.class.getName());
+ final Object obj = mapper.readValue(json, claz);
+ Assert.assertTrue(obj.equals(e));
+ }
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestJrubyCurrencyPlugin.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestJrubyCurrencyPlugin.java
new file mode 100644
index 0000000..74839b2
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestJrubyCurrencyPlugin.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration.osgi;
+
+import java.math.BigDecimal;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.beatrix.osgi.SetupBundleWithAssertion;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.currency.api.Rate;
+import org.killbill.billing.currency.plugin.api.CurrencyPluginApi;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+public class TestJrubyCurrencyPlugin extends TestOSGIBase {
+
+ private final String BUNDLE_TEST_RESOURCE_PREFIX = "killbill-currency-plugin-test";
+ private final String BUNDLE_TEST_RESOURCE = BUNDLE_TEST_RESOURCE_PREFIX + ".tar.gz";
+
+ @Inject
+ private OSGIServiceRegistration<CurrencyPluginApi> currencyPluginApiOSGIServiceRegistration;
+
+ @BeforeClass(groups = "slow")
+ public void beforeClass() throws Exception {
+
+ // OSGIDataSourceConfig
+ super.beforeClass();
+
+ // This is extracted from surefire system configuration-- needs to be added explicitly in IntelliJ for correct running
+ final String killbillVersion = System.getProperty("killbill.version");
+
+ SetupBundleWithAssertion setupTest = new SetupBundleWithAssertion(BUNDLE_TEST_RESOURCE, osgiConfig, killbillVersion);
+ setupTest.setupJrubyBundle();
+ }
+
+ @Test(groups = "slow")
+ public void testCurrencyApis() throws Exception {
+
+ CurrencyPluginApi api = getTestPluginCurrencyApi();
+
+ final Set<Currency> currencies = api.getBaseCurrencies();
+ assertEquals(currencies.size(), 1);
+ assertEquals(currencies.iterator().next(), Currency.USD);
+
+ final DateTime res = api.getLatestConversionDate(Currency.USD);
+ assertNotNull(res);
+
+ final Set<Rate> rates = api.getCurrentRates(Currency.USD);
+ assertEquals(rates.size(), 1);
+ final Rate theRate = rates.iterator().next();
+ assertEquals(theRate.getBaseCurrency(), Currency.USD);
+ assertEquals(theRate.getCurrency(), Currency.BRL);
+ Assert.assertTrue(theRate.getValue().compareTo(new BigDecimal("12.3")) == 0);
+
+ }
+
+ private CurrencyPluginApi getTestPluginCurrencyApi() {
+ int retry = 5;
+
+ // It is expected to have a nul result if the initialization of Killbill went faster than the registration of the plugin services
+ CurrencyPluginApi result = null;
+ do {
+ result = currencyPluginApiOSGIServiceRegistration.getServiceForName(BUNDLE_TEST_RESOURCE_PREFIX);
+ if (result == null) {
+ try {
+ log.info("Waiting for Killbill initialization to complete time = " + clock.getUTCNow());
+ Thread.sleep(1000);
+ } catch (InterruptedException ignore) {
+ }
+ }
+ } while (result == null && retry-- > 0);
+ Assert.assertNotNull(result);
+ return result;
+ }
+
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestJrubyNotificationPlugin.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestJrubyNotificationPlugin.java
new file mode 100644
index 0000000..51feb4a
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestJrubyNotificationPlugin.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration.osgi;
+
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.beatrix.osgi.SetupBundleWithAssertion;
+import org.killbill.billing.util.tag.Tag;
+
+public class TestJrubyNotificationPlugin extends TestOSGIBase {
+
+ private final String BUNDLE_TEST_RESOURCE_PREFIX = "killbill-notification-test";
+ private final String BUNDLE_TEST_RESOURCE = BUNDLE_TEST_RESOURCE_PREFIX + ".tar.gz";
+
+ @BeforeClass(groups = "slow")
+ public void beforeClass() throws Exception {
+
+ // OSGIDataSourceConfig
+ super.beforeClass();
+
+ // This is extracted from surefire system configuration-- needs to be added explicitly in IntelliJ for correct running
+ final String killbillVersion = System.getProperty("killbill.version");
+
+ SetupBundleWithAssertion setupTest = new SetupBundleWithAssertion(BUNDLE_TEST_RESOURCE, osgiConfig, killbillVersion);
+ setupTest.setupJrubyBundle();
+ }
+
+ @Test(groups = "slow")
+ public void testOnEventForAccountCreation() throws Exception {
+
+ // Once we create the account we give the hand to the jruby notification plugin
+ // which will handle the ExtBusEvent and start updating the account, create tag definition and finally create a tag.
+ // We wait for all that to occur and declare victory if we see the TagDefinition/Tag creation.
+ busHandler.pushExpectedEvents(NextEvent.TAG_DEFINITION, NextEvent.TAG);
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(4));
+ assertListenerStatus();
+
+ final List<Tag> tags = tagUserApi.getTagsForAccount(account.getId(), false, callContext);
+ Assert.assertEquals(tags.size(), 1);
+ //final Tag tag = tags.get(0);
+ }
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestOSGIBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestOSGIBase.java
new file mode 100644
index 0000000..241687a
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestOSGIBase.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration.osgi;
+
+import org.killbill.billing.beatrix.integration.TestIntegrationBase;
+
+public class TestOSGIBase extends TestIntegrationBase {
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestOSGIIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestOSGIIntegration.java
new file mode 100644
index 0000000..c427b8e
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/osgi/TestOSGIIntegration.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration.osgi;
+
+import org.testng.annotations.Test;
+
+public class TestOSGIIntegration extends TestOSGIBase {
+
+ @Test(groups = "slow")
+ public void testJRubyIntegration() throws Exception {
+ createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+ }
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/IntegrationTestOverdueModule.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/IntegrationTestOverdueModule.java
new file mode 100644
index 0000000..8cbeba1
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/IntegrationTestOverdueModule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration.overdue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.overdue.OverdueService;
+import org.killbill.billing.overdue.glue.DefaultOverdueModule;
+
+public class IntegrationTestOverdueModule extends DefaultOverdueModule {
+
+ public IntegrationTestOverdueModule(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ protected void installOverdueService() {
+ bind(OverdueService.class).to(MockOverdueService.class);
+ }
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/MockOverdueService.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/MockOverdueService.java
new file mode 100644
index 0000000..d054751
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/MockOverdueService.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration.overdue;
+
+import javax.inject.Named;
+
+import org.killbill.billing.overdue.OverdueProperties;
+import org.killbill.billing.overdue.OverdueUserApi;
+import org.killbill.billing.overdue.glue.DefaultOverdueModule;
+import org.killbill.billing.overdue.listener.OverdueListener;
+import org.killbill.billing.overdue.notification.OverdueNotifier;
+import org.killbill.billing.overdue.service.DefaultOverdueService;
+import org.killbill.billing.overdue.wrapper.OverdueWrapperFactory;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.inject.Inject;
+
+public class MockOverdueService extends DefaultOverdueService {
+
+ @Inject
+ public MockOverdueService(final OverdueUserApi userApi, final OverdueProperties properties,
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_CHECK_NAMED) final OverdueNotifier checkNotifier,
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_ASYNC_BUS_NAMED) final OverdueNotifier asyncNotifier,
+ final BusService busService, final OverdueListener listener, final OverdueWrapperFactory factory) {
+ super(userApi, properties, checkNotifier, asyncNotifier, busService, listener, factory);
+ }
+
+ public synchronized void loadConfig() throws ServiceException {
+
+ }
+
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestBillingAlignment.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestBillingAlignment.java
new file mode 100644
index 0000000..d052b6c
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestBillingAlignment.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration.overdue;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.beatrix.integration.TestIntegrationBase;
+import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+import static org.testng.Assert.assertNotNull;
+
+public class TestBillingAlignment extends TestIntegrationBase {
+
+ // TODO test fails as it should not create a proration when the chnage to annual occurs. Instaed we should restart from the data of the chnage
+ // since we have as a catalog rule:
+ // <billingAlignmentCase>
+ // <billingPeriod>ANNUAL</billingPeriod>
+ // <alignment>SUBSCRIPTION</alignment>
+ // </billingAlignmentCase>
+ //
+ @Test(groups = "slow", enabled = false)
+ public void testTransitonAccountBAToSubscriptionBA() throws Exception {
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ // We take april as it has 30 days (easier to play with BCD)
+ // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+ // (Start with monthly that has a 'Account' billing alignment
+ //
+ final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ assertNotNull(bpEntitlement);
+ invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+
+ // GET OUT TRIAL
+ addDaysAndCheckForCompletion(33, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+
+ //
+ // Change plan to annual that has been configured to have a 'SubscriptionBase' billing alignment
+ changeEntitlementAndCheckForCompletion(bpEntitlement, "Shotgun", BillingPeriod.ANNUAL, null, NextEvent.CHANGE, NextEvent.INVOICE);
+
+
+ /*
+
+ | 64e17f77-fcdd-4c87-8543-1a64d957460c | FIXED | 2012-04-01 | NULL | 0.0000 | NULL | shotgun-monthly |
+ | 07924bfa-cc9b-46dc-ad22-a9a39830a128 | RECURRING | 2012-05-01 | 2012-06-01 | 249.9500 | 249.9500 | shotgun-monthly |
+ | 92c1e86b-284a-4d33-a920-3cbc6e05f7e6 | RECURRING | 2012-05-01 | 2012-05-04 | 24.2000 | 249.9500 | shotgun-monthly |
+ | 92c1e86b-284a-4d33-a920-3cbc6e05f7e6 | RECURRING | 2012-05-04 | 2012-06-01 | 183.6000 | 2399.9500 | shotgun-annual |
+ | 07924bfa-cc9b-46dc-ad22-a9a39830a128 | REPAIR_ADJ | 2012-05-01 | 2012-06-01 | -249.9500 | NULL | NULL |
+ | 07924bfa-cc9b-46dc-ad22-a9a39830a128 | CBA_ADJ | 2012-05-04 | 2012-05-04 | 249.9500 | NULL | NULL |
+ | 92c1e86b-284a-4d33-a920-3cbc6e05f7e6 | CBA_ADJ | 2012-05-04 | 2012-05-04 | -207.8000 | NULL | NULL |
+ */
+ }
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
new file mode 100644
index 0000000..ec57bce
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration.overdue;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.concurrent.Callable;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.beatrix.integration.BeatrixIntegrationModule;
+import org.killbill.billing.beatrix.integration.TestIntegrationBase;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.SubscriptionBundle;
+import org.killbill.billing.overdue.OverdueService;
+import org.killbill.billing.overdue.config.OverdueConfig;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+import org.killbill.billing.payment.api.TestPaymentMethodPluginBase;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+
+import static com.jayway.awaitility.Awaitility.await;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.testng.Assert.assertNotNull;
+
+public abstract class TestOverdueBase extends TestIntegrationBase {
+
+ protected Account account;
+ protected SubscriptionBundle bundle;
+ protected String productName;
+ protected BillingPeriod term;
+
+ public abstract String getOverdueConfig();
+
+ final PaymentMethodPlugin paymentMethodPlugin = new TestPaymentMethodPluginBase();
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ final String configXml = getOverdueConfig();
+ final InputStream is = new ByteArrayInputStream(configXml.getBytes());
+ final OverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, OverdueConfig.class);
+ overdueWrapperFactory.setOverdueConfig(config);
+
+ account = createAccountWithNonOsgiPaymentMethod(getAccountData(0));
+ assertNotNull(account);
+
+ paymentApi.addPaymentMethod(BeatrixIntegrationModule.NON_OSGI_PLUGIN_NAME, account, true, paymentMethodPlugin, callContext);
+ productName = "Shotgun";
+ term = BillingPeriod.MONTHLY;
+ paymentPlugin.clear();
+ }
+
+ protected void checkODState(final String expected) {
+ try {
+ // This will test the overdue notification queue: when we move the clock, the overdue system
+ // should get notified to refresh its state.
+ // Calling explicitly refresh here (overdueApi.refreshOverdueStateFor(account)) would not fully
+ // test overdue.
+ // Since we're relying on the notification queue, we may need to wait a bit (hence await()).
+ await().atMost(10, SECONDS).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return expected.equals(blockingApi.getBlockingStateForService(account.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext).getStateName());
+ }
+ });
+ } catch (Exception e) {
+ Assert.assertEquals(blockingApi.getBlockingStateForService(account.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext).getStateName(), expected, "Got exception: " + e.toString());
+ }
+ }
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java
new file mode 100644
index 0000000..8a1f453
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration;
+
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.joda.time.DateTime;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.notification.plugin.api.ExtBusEvent;
+
+import com.google.common.eventbus.Subscribe;
+
+import static com.jayway.awaitility.Awaitility.await;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.testng.Assert.assertNotNull;
+
+public class TestPublicBus extends TestIntegrationBase {
+
+ private PublicListener publicListener;
+
+ private AtomicInteger externalBusCount;
+
+ public class PublicListener {
+
+ @Subscribe
+ public void handleExternalEvents(final ExtBusEvent event) {
+ log.info("GOT EXT EVENT " + event);
+ externalBusCount.incrementAndGet();
+
+ }
+ }
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ publicListener = new PublicListener();
+
+ log.debug("RESET TEST FRAMEWORK");
+
+ clock.resetDeltaFromReality();
+ busHandler.reset();
+
+ // Start services
+ lifecycle.fireStartupSequencePriorEventRegistration();
+ busService.getBus().register(busHandler);
+ externalBus.register(publicListener);
+ lifecycle.fireStartupSequencePostEventRegistration();
+
+ this.externalBusCount = new AtomicInteger(0);
+ }
+
+ @Test(groups = "{slow}")
+ public void testSimple() throws Exception {
+
+ final DateTime initialDate = new DateTime(2012, 2, 1, 0, 3, 42, 0, testTimeZone);
+ final int billingDay = 2;
+
+ log.info("Beginning test with BCD of " + billingDay);
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+ final UUID accountId = account.getId();
+ assertNotNull(account);
+
+ // set clock to the initial start date
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ String productName = "Shotgun";
+ BillingPeriod term = BillingPeriod.MONTHLY;
+ String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ //
+ // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+ //
+ final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.INVOICE);
+ assertNotNull(bpEntitlement);
+
+ await().atMost(10, SECONDS).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ // expecting ACCOUNT_CREATION, ACCOUNT_CHANGE, SUBSCRIPTION_CREATION, INVOICE_CREATION
+ return externalBusCount.get() == 4;
+ }
+ });
+ }
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/util/AccountChecker.java b/beatrix/src/test/java/org/killbill/billing/beatrix/util/AccountChecker.java
new file mode 100644
index 0000000..b6d9740
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/util/AccountChecker.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.util;
+
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.util.callcontext.CallContext;
+
+public class AccountChecker {
+
+ private static final Logger log = LoggerFactory.getLogger(AccountChecker.class);
+
+ private final AccountUserApi accountApi;
+ private final AuditChecker auditChecker;
+
+ @Inject
+ public AccountChecker(final AccountUserApi accountApi, final AuditChecker auditChecker) {
+ this.accountApi = accountApi;
+ this.auditChecker = auditChecker;
+ }
+
+ public Account checkAccount(final UUID accountId, final AccountData accountData, final CallContext context) throws Exception {
+
+ final Account account = accountApi.getAccountById(accountId, context);
+ // Not all test pass it, since this is always the same test
+ if (accountData != null) {
+ Assert.assertEquals(account.getName(), accountData.getName());
+ Assert.assertEquals(account.getFirstNameLength(), accountData.getFirstNameLength());
+ Assert.assertEquals(account.getEmail(), accountData.getEmail());
+ Assert.assertEquals(account.getPhone(), accountData.getPhone());
+ Assert.assertEquals(account.isNotifiedForInvoices(), accountData.isNotifiedForInvoices());
+ Assert.assertEquals(account.getExternalKey(), accountData.getExternalKey());
+ Assert.assertEquals(account.getBillCycleDayLocal(), accountData.getBillCycleDayLocal());
+ Assert.assertEquals(account.getCurrency(), accountData.getCurrency());
+ Assert.assertEquals(account.getTimeZone(), accountData.getTimeZone());
+ // createWithPaymentMethod will update the paymentMethod
+ //Assert.assertEquals(account.getPaymentMethodId(), accountData.getPaymentMethodId());
+ }
+
+ auditChecker.checkAccountCreated(account, context);
+ return account;
+ }
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/util/PaymentChecker.java b/beatrix/src/test/java/org/killbill/billing/beatrix/util/PaymentChecker.java
new file mode 100644
index 0000000..fb1105c
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/util/PaymentChecker.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.util;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentStatus;
+import org.killbill.billing.util.callcontext.CallContext;
+
+import com.google.inject.Inject;
+
+public class PaymentChecker {
+
+ private static final Logger log = LoggerFactory.getLogger(PaymentChecker.class);
+
+ private final PaymentApi paymentApi;
+ private final AuditChecker auditChecker;
+
+ @Inject
+ public PaymentChecker(final PaymentApi paymentApi, final AuditChecker auditChecker) {
+ this.paymentApi = paymentApi;
+ this.auditChecker = auditChecker;
+ }
+
+ public Payment checkPayment(final UUID accountId, final int paymentOrderingNumber, final CallContext context, ExpectedPaymentCheck expected) throws PaymentApiException {
+ final List<Payment> payments = paymentApi.getAccountPayments(accountId, context);
+ Assert.assertEquals(payments.size(), paymentOrderingNumber);
+ final Payment payment = payments.get(paymentOrderingNumber - 1);
+ if (payment.getPaymentStatus() == PaymentStatus.UNKNOWN) {
+ checkPaymentNoAuditForRuntimeException(accountId, payment, context, expected);
+ } else {
+ checkPayment(accountId, payment, context, expected);
+ }
+ return payment;
+ }
+
+ private void checkPayment(final UUID accountId, final Payment payment, final CallContext context, final ExpectedPaymentCheck expected) {
+ Assert.assertEquals(payment.getAccountId(), accountId);
+ Assert.assertTrue(payment.getAmount().compareTo(expected.getAmount()) == 0);
+ Assert.assertEquals(payment.getPaymentStatus(), expected.getStatus());
+ Assert.assertEquals(payment.getInvoiceId(), expected.getInvoiceId());
+ Assert.assertEquals(payment.getCurrency(), expected.getCurrency());
+ auditChecker.checkPaymentCreated(payment, context);
+ }
+
+ private void checkPaymentNoAuditForRuntimeException(final UUID accountId, final Payment payment, final CallContext context, final ExpectedPaymentCheck expected) {
+ Assert.assertEquals(payment.getAccountId(), accountId);
+ Assert.assertTrue(payment.getAmount().compareTo(expected.getAmount()) == 0);
+ Assert.assertEquals(payment.getPaymentStatus(), expected.getStatus());
+ Assert.assertEquals(payment.getInvoiceId(), expected.getInvoiceId());
+ Assert.assertEquals(payment.getCurrency(), expected.getCurrency());
+ }
+
+ public static class ExpectedPaymentCheck {
+
+ private final LocalDate paymentDate;
+ private final BigDecimal amount;
+ private final PaymentStatus status;
+ private final UUID invoiceId;
+ private final Currency currency;
+
+ public ExpectedPaymentCheck(final LocalDate paymentDate, final BigDecimal amount, final PaymentStatus status, final UUID invoiceId, final Currency currency) {
+ this.paymentDate = paymentDate;
+ this.amount = amount;
+ this.status = status;
+ this.invoiceId = invoiceId;
+ this.currency = currency;
+ }
+
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ public LocalDate getPaymentDate() {
+ return paymentDate;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public PaymentStatus getStatus() {
+ return status;
+ }
+
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+ }
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/util/RefundChecker.java b/beatrix/src/test/java/org/killbill/billing/beatrix/util/RefundChecker.java
new file mode 100644
index 0000000..87c9439
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/util/RefundChecker.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.util;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentApi;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
+import org.killbill.billing.invoice.api.InvoiceUserApi;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.Refund;
+import org.killbill.billing.util.callcontext.CallContext;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.inject.Inject;
+
+public class RefundChecker {
+
+ private static final Logger log = LoggerFactory.getLogger(RefundChecker.class);
+
+ private final PaymentApi paymentApi;
+ private final InvoicePaymentApi invoicePaymentApi;
+ private final AuditChecker auditChecker;
+ private final InvoiceUserApi invoiceUserApi;
+
+ @Inject
+ public RefundChecker(final PaymentApi paymentApi, final InvoicePaymentApi invoicePaymentApi, final InvoiceUserApi invoiceApi, final AuditChecker auditChecker) {
+ this.paymentApi = paymentApi;
+ this.invoicePaymentApi = invoicePaymentApi;
+ this.auditChecker = auditChecker;
+ this.invoiceUserApi = invoiceApi;
+ }
+
+ public Refund checkRefund(final UUID paymentId, final CallContext context, ExpectedRefundCheck expected) throws PaymentApiException {
+
+ final List<Refund> refunds = paymentApi.getPaymentRefunds(paymentId, context);
+ Assert.assertEquals(refunds.size(), 1);
+
+ final InvoicePayment refundInvoicePayment = getInvoicePaymentEntry(paymentId, InvoicePaymentType.REFUND, context);
+ final InvoicePayment invoicePayment = getInvoicePaymentEntry(paymentId, InvoicePaymentType.ATTEMPT, context);
+
+ final Refund refund = refunds.get(0);
+ Assert.assertEquals(refund.getPaymentId(), expected.getPaymentId());
+ Assert.assertEquals(refund.getCurrency(), expected.getCurrency());
+ Assert.assertEquals(refund.isAdjusted(), expected.isAdjusted);
+ Assert.assertEquals(refund.getRefundAmount().compareTo(expected.getRefundAmount()), 0);
+
+ Assert.assertEquals(refundInvoicePayment.getPaymentId(), paymentId);
+ Assert.assertEquals(refundInvoicePayment.getLinkedInvoicePaymentId(), invoicePayment.getId());
+ Assert.assertEquals(refundInvoicePayment.getPaymentCookieId(), refund.getId());
+ Assert.assertEquals(refundInvoicePayment.getInvoiceId(), invoicePayment.getInvoiceId());
+ Assert.assertEquals(refundInvoicePayment.getAmount().compareTo(expected.getRefundAmount().negate()), 0);
+ Assert.assertEquals(refundInvoicePayment.getCurrency(), expected.getCurrency());
+
+ return refund;
+ }
+
+ private InvoicePayment getInvoicePaymentEntry(final UUID paymentId, final InvoicePaymentType type, final CallContext context) {
+ final List<InvoicePayment> invoicePayments = invoicePaymentApi.getInvoicePayments(paymentId, context);
+ final Collection<InvoicePayment> refundInvoicePayments = Collections2.filter(invoicePayments, new Predicate<InvoicePayment>() {
+ @Override
+ public boolean apply(@Nullable final InvoicePayment invoicePayment) {
+ return invoicePayment.getType() == type && invoicePayment.getPaymentId().equals(paymentId);
+ }
+ });
+ Assert.assertEquals(refundInvoicePayments.size(), 1);
+ return refundInvoicePayments.iterator().next();
+ }
+
+ public static class ExpectedRefundCheck {
+
+ private final UUID paymentId;
+ private final boolean isAdjusted;
+ private final BigDecimal refundAmount;
+ private final Currency currency;
+ private final LocalDate refundDate;
+
+ public ExpectedRefundCheck(final UUID paymentId, final boolean adjusted, final BigDecimal refundAmount, final Currency currency, final LocalDate refundDate) {
+ this.paymentId = paymentId;
+ isAdjusted = adjusted;
+ this.refundAmount = refundAmount;
+ this.currency = currency;
+ this.refundDate = refundDate;
+ }
+
+ public UUID getPaymentId() {
+ return paymentId;
+ }
+
+ public boolean isAdjusted() {
+ return isAdjusted;
+ }
+
+ public BigDecimal getRefundAmount() {
+ return refundAmount;
+ }
+
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ public LocalDate getRefundDate() {
+ return refundDate;
+ }
+ }
+}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/util/SubscriptionChecker.java b/beatrix/src/test/java/org/killbill/billing/beatrix/util/SubscriptionChecker.java
new file mode 100644
index 0000000..2c0391b
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/util/SubscriptionChecker.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.util;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+public class SubscriptionChecker {
+
+ private static final Logger log = LoggerFactory.getLogger(SubscriptionChecker.class);
+
+ private final SubscriptionBaseInternalApi subscriptionApi;
+ private final AuditChecker auditChecker;
+ private final NonEntityDao nonEntityDao;
+
+ @Inject
+ public SubscriptionChecker(final SubscriptionBaseInternalApi subscriptionApi, final AuditChecker auditChecker, final NonEntityDao nonEntityDao) {
+ this.subscriptionApi = subscriptionApi;
+ this.auditChecker = auditChecker;
+ this.nonEntityDao = nonEntityDao;
+ }
+
+ public SubscriptionBaseBundle checkBundleNoAudits(final UUID bundleId, final UUID expectedAccountId, final String expectedKey, final InternalTenantContext context) throws SubscriptionBaseApiException {
+ final SubscriptionBaseBundle bundle = subscriptionApi.getBundleFromId(bundleId, context);
+ Assert.assertNotNull(bundle);
+ Assert.assertEquals(bundle.getAccountId(), expectedAccountId);
+ Assert.assertEquals(bundle.getExternalKey(), expectedKey);
+ return bundle;
+ }
+
+ public SubscriptionBase checkSubscriptionCreated(final UUID subscriptionId, final InternalCallContext context) throws SubscriptionBaseApiException {
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT);
+ final CallContext callContext = context.toCallContext(tenantId);
+
+ final SubscriptionBase subscription = subscriptionApi.getSubscriptionFromId(subscriptionId, context);
+ Assert.assertNotNull(subscription);
+ auditChecker.checkSubscriptionCreated(subscription.getBundleId(), subscriptionId, callContext);
+
+ List<SubscriptionBaseTransition> subscriptionEvents = getSubscriptionEvents(subscription);
+ Assert.assertTrue(subscriptionEvents.size() >= 1);
+ auditChecker.checkSubscriptionEventCreated(subscription.getBundleId(), ((SubscriptionBaseTransitionData) subscriptionEvents.get(0)).getId(), callContext);
+
+ auditChecker.checkBundleCreated(subscription.getBundleId(), callContext);
+ return subscription;
+ }
+
+ private List<SubscriptionBaseTransition> getSubscriptionEvents(final SubscriptionBase subscription) {
+ return subscription.getAllTransitions();
+ }
+}
beatrix/src/test/resources/beatrix.properties 40(+20 -20)
diff --git a/beatrix/src/test/resources/beatrix.properties b/beatrix/src/test/resources/beatrix.properties
index ad6d9fb..e336e19 100644
--- a/beatrix/src/test/resources/beatrix.properties
+++ b/beatrix/src/test/resources/beatrix.properties
@@ -1,28 +1,28 @@
-killbill.catalog.uri=file:src/test/resources/catalogSample.xml
+org.killbill.catalog.uri=file:src/test/resources/catalogSample.xml
-killbill.billing.notificationq.main.sleep=100
-killbill.billing.notificationq.main.nbThreads=1
-killbill.billing.notificationq.main.useInFlightQ=false
-killbill.billing.notificationq.main.prefetch=1
-killbill.billing.notificationq.main.claimed=1
+org.killbill.notificationq.main.sleep=100
+org.killbill.notificationq.main.nbThreads=1
+org.killbill.notificationq.main.useInFlightQ=false
+org.killbill.notificationq.main.prefetch=1
+org.killbill.notificationq.main.claimed=1
-killbill.billing.persistent.bus.main.sleep=100
-killbill.billing.persistent.bus.main.nbThreads=1
-killbill.billing.persistent.bus.main.prefetch=1
-killbill.billing.persistent.bus.main.claimed=1
-killbill.billing.persistent.bus.main.useInFlightQ=false
+org.killbill.persistent.bus.main.sleep=100
+org.killbill.persistent.bus.main.nbThreads=1
+org.killbill.persistent.bus.main.prefetch=1
+org.killbill.persistent.bus.main.claimed=1
+org.killbill.persistent.bus.main.useInFlightQ=false
-killbill.billing.persistent.bus.external.sleep=100
-killbill.billing.persistent.bus.external.nbThreads=1
-killbill.billing.persistent.bus.external.prefetch=1
-killbill.billing.persistent.bus.external.claimed=1
-killbill.billing.persistent.bus.external.useInFlightQ=false
+org.killbill.persistent.bus.external.sleep=100
+org.killbill.persistent.bus.external.nbThreads=1
+org.killbill.persistent.bus.external.prefetch=1
+org.killbill.persistent.bus.external.claimed=1
+org.killbill.persistent.bus.external.useInFlightQ=false
-killbill.billing.persistent.bus.external.tableName=bus_ext_events
-killbill.billing.persistent.bus.external.historyTableName=bus_ext_events_history
+org.killbill.persistent.bus.external.tableName=bus_ext_events
+org.killbill.persistent.bus.external.historyTableName=bus_ext_events_history
user.timezone=UTC
-killbill.payment.retry.days=8,8,8,8,8,8,8,8
-killbill.osgi.bundle.install.dir=/var/tmp/beatrix-bundles
+org.killbill.payment.retry.days=8,8,8,8,8,8,8,8
+org.killbill.osgi.bundle.install.dir=/var/tmp/beatrix-bundles
org.slf4j.simpleLogger.showDateTime=true
diff --git a/beatrix/src/test/resources/Catalog-Entitlement-Testplan.txt b/beatrix/src/test/resources/Catalog-Entitlement-Testplan.txt
index 6d5867f..7c0ddf6 100644
--- a/beatrix/src/test/resources/Catalog-Entitlement-Testplan.txt
+++ b/beatrix/src/test/resources/Catalog-Entitlement-Testplan.txt
@@ -109,4 +109,4 @@ ADD-ON TESTS
* Add-on creation alignment
* Add-on cancel with base plan
-
\ No newline at end of file
+
diff --git a/beatrix/src/test/resources/killbill-currency-plugin-test.tar.gz b/beatrix/src/test/resources/killbill-currency-plugin-test.tar.gz
index 8c3a706..c816039 100644
Binary files a/beatrix/src/test/resources/killbill-currency-plugin-test.tar.gz and b/beatrix/src/test/resources/killbill-currency-plugin-test.tar.gz differ
diff --git a/beatrix/src/test/resources/killbill-notification-test.tar.gz b/beatrix/src/test/resources/killbill-notification-test.tar.gz
index aa31661..960465e 100644
Binary files a/beatrix/src/test/resources/killbill-notification-test.tar.gz and b/beatrix/src/test/resources/killbill-notification-test.tar.gz differ
diff --git a/beatrix/src/test/resources/killbill-payment-test.tar.gz b/beatrix/src/test/resources/killbill-payment-test.tar.gz
index 6787718..13e0e1f 100644
Binary files a/beatrix/src/test/resources/killbill-payment-test.tar.gz and b/beatrix/src/test/resources/killbill-payment-test.tar.gz differ
bin/clean-and-install 2(+1 -1)
diff --git a/bin/clean-and-install b/bin/clean-and-install
index c6c3ac4..0842329 100755
--- a/bin/clean-and-install
+++ b/bin/clean-and-install
@@ -19,4 +19,4 @@
###################################################################################
bin/db-helper -a clean -d killbill;
-mvn -Dcom.ning.billing.dbi.test.useLocalDb=true clean install
+mvn -Dorg.killbill.billing.dbi.test.useLocalDb=true clean install
bin/cleanAndInstall 2(+1 -1)
diff --git a/bin/cleanAndInstall b/bin/cleanAndInstall
index fca07ad..6a3a9d7 100755
--- a/bin/cleanAndInstall
+++ b/bin/cleanAndInstall
@@ -20,4 +20,4 @@
bin/db-helper -a clean -d killbill;
bin/db-helper -a clean -d test_killbill;
-mvn -Dcom.ning.billing.dbi.test.useLocalDb=true clean install
+mvn -Dorg.killbill.billing.dbi.test.useLocalDb=true clean install
bin/start-server 4(+2 -2)
diff --git a/bin/start-server b/bin/start-server
index 72e86b4..77711fd 100755
--- a/bin/start-server
+++ b/bin/start-server
@@ -55,8 +55,8 @@ function build_properties() {
local opts=
local prop=
for prop in `cat $PROPERTIES | grep =`; do
- local k=`echo $prop | awk ' BEGIN {FS="="} { print $1 }'`
- local v=`echo $prop | awk 'BEGIN {FS="="} { print $2 }'`
+ local k=`echo $prop | awk 'BEGIN {FS="="} { print $1 }'`
+ local v=`echo $prop | awk 'BEGIN {FS="="} {for (i=2; i<NF; i++) printf $i "="; print $NF}'`
opts="$opts -D$k=$v"
done
echo $opts
catalog/pom.xml 28(+14 -14)
diff --git a/catalog/pom.xml b/catalog/pom.xml
index 2c0d1b5..1d2f04e 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-catalog</artifactId>
@@ -41,38 +41,38 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
@@ -111,7 +111,7 @@
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
- <Main-Class>com.ning.billing.catalog.LoadCatalog</Main-Class>
+ <Main-Class>org.killbill.billing.catalog.LoadCatalog</Main-Class>
</manifestEntries>
</transformer>
</transformers>
@@ -130,7 +130,7 @@
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
- <Main-Class>com.ning.billing.catalog.CreateCatalogSchema</Main-Class>
+ <Main-Class>org.killbill.billing.catalog.CreateCatalogSchema</Main-Class>
</manifestEntries>
</transformer>
</transformers>
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
new file mode 100644
index 0000000..04368ba
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.api.user;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.CatalogUserApi;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+public class DefaultCatalogUserApi implements CatalogUserApi {
+
+ private final CatalogService catalogService;
+
+ @Inject
+ public DefaultCatalogUserApi(final CatalogService catalogService) {
+ this.catalogService = catalogService;
+ }
+
+ @Override
+ public Catalog getCatalog(final String catalogName, final TenantContext context) {
+ // STEPH TODO this is hack until we decides what do do exactly:
+ // Probably we want one catalog for tenant but but TBD
+ return catalogService.getFullCatalog();
+ }
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/CreateCatalogSchema.java b/catalog/src/main/java/org/killbill/billing/catalog/CreateCatalogSchema.java
new file mode 100644
index 0000000..8221446
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/CreateCatalogSchema.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.Writer;
+
+import org.killbill.billing.util.config.catalog.XMLSchemaGenerator;
+
+public class CreateCatalogSchema {
+
+ /**
+ * @param args output file path
+ */
+ public static void main(final String[] args) throws Exception {
+ if (args.length != 1) {
+ System.err.println("Usage: <filepath>");
+ System.exit(0);
+ }
+
+ final File f = new File(args[0]);
+ final Writer w = new FileWriter(f);
+ w.write(XMLSchemaGenerator.xmlSchemaAsString(StandaloneCatalog.class));
+ w.close();
+ }
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultCatalogService.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultCatalogService.java
new file mode 100644
index 0000000..4fcb325
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultCatalogService.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.StaticCatalog;
+import org.killbill.billing.catalog.io.VersionedCatalogLoader;
+import org.killbill.billing.lifecycle.KillbillService;
+import org.killbill.billing.lifecycle.LifecycleHandlerType;
+import org.killbill.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+import org.killbill.billing.util.config.CatalogConfig;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class DefaultCatalogService implements KillbillService, Provider<Catalog>, CatalogService {
+
+ private static final String CATALOG_SERVICE_NAME = "catalog-service";
+
+ private static VersionedCatalog catalog;
+
+ private final CatalogConfig config;
+ private boolean isInitialized;
+
+ private final VersionedCatalogLoader loader;
+
+ @Inject
+ public DefaultCatalogService(final CatalogConfig config, final VersionedCatalogLoader loader) {
+ this.config = config;
+ this.isInitialized = false;
+ this.loader = loader;
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.LOAD_CATALOG)
+ public synchronized void loadCatalog() throws ServiceException {
+ if (!isInitialized) {
+ try {
+ final String url = config.getCatalogURI();
+ catalog = loader.load(url);
+
+ isInitialized = true;
+ } catch (Exception e) {
+ throw new ServiceException(e);
+ }
+ }
+ }
+
+ @Override
+ public String getName() {
+ return CATALOG_SERVICE_NAME;
+ }
+
+ @Override
+ public Catalog getFullCatalog() {
+ return catalog;
+ }
+
+ @Override
+ public Catalog get() {
+ return catalog;
+ }
+
+ @Override
+ public StaticCatalog getCurrentCatalog() {
+ return catalog;
+ }
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java
new file mode 100644
index 0000000..b769ec2
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+
+import org.joda.time.DateTime;
+import org.joda.time.Period;
+
+import org.killbill.billing.catalog.api.Duration;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationError;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultDuration extends ValidatingConfig<StandaloneCatalog> implements Duration {
+ @XmlElement(required = true)
+ private TimeUnit unit;
+
+ @XmlElement(required = false)
+ private Integer number = -1;
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.IDuration#getUnit()
+ */
+ @Override
+ public TimeUnit getUnit() {
+ return unit;
+ }
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.IDuration#getLength()
+ */
+ @Override
+ public int getNumber() {
+ return number;
+ }
+
+ @Override
+ public DateTime addToDateTime(final DateTime dateTime) {
+ if ((number == null) && (unit != TimeUnit.UNLIMITED)) {
+ return dateTime;
+ }
+
+ switch (unit) {
+ case DAYS:
+ return dateTime.plusDays(number);
+ case MONTHS:
+ return dateTime.plusMonths(number);
+ case YEARS:
+ return dateTime.plusYears(number);
+ case UNLIMITED:
+ return dateTime.plusYears(100);
+ default:
+ return dateTime;
+ }
+ }
+
+ @Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ //Validation: TimeUnit UNLIMITED iff number == -1
+ if ((unit == TimeUnit.UNLIMITED && number != -1)) {
+ errors.add(new ValidationError("Duration can only have 'UNLIMITED' unit if the number is omitted.",
+ catalog.getCatalogURI(), DefaultPlanPhase.class, ""));
+ }
+
+ //TODO MDW - Validation TimeUnit UNLIMITED iff number == -1
+ return errors;
+ }
+
+ protected DefaultDuration setUnit(final TimeUnit unit) {
+ this.unit = unit;
+ return this;
+ }
+
+ protected DefaultDuration setNumber(final Integer number) {
+ this.number = number;
+ return this;
+ }
+
+ @Override
+ public Period toJodaPeriod() {
+ if ((number == null) && (unit != TimeUnit.UNLIMITED)) {
+ return new Period();
+ }
+
+ switch (unit) {
+ case DAYS:
+ return new Period().withDays(number);
+ case MONTHS:
+ return new Period().withMonths(number);
+ case YEARS:
+ return new Period().withYears(number);
+ case UNLIMITED:
+ return new Period().withYears(100);
+ default:
+ return new Period();
+ }
+ }
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultInternationalPrice.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultInternationalPrice.java
new file mode 100644
index 0000000..cb3bb49
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultInternationalPrice.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import java.math.BigDecimal;
+import java.net.URI;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.CurrencyValueNull;
+import org.killbill.billing.catalog.api.InternationalPrice;
+import org.killbill.billing.catalog.api.Price;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultInternationalPrice extends ValidatingConfig<StandaloneCatalog> implements InternationalPrice {
+
+ //TODO: Must have a price point for every configured currency
+ //TODO: No prices is a zero cost plan
+ @XmlElement(name = "price")
+ private DefaultPrice[] prices;
+
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.InternationalPrice#getPrices()
+ */
+ @Override
+ public Price[] getPrices() {
+ return prices;
+ }
+
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.IInternationalPrice#getPrice(org.killbill.billing.catalog.api.Currency)
+ */
+ @Override
+ public BigDecimal getPrice(final Currency currency) throws CatalogApiException {
+ for (final Price p : prices) {
+ if (p.getCurrency() == currency) {
+ return p.getValue();
+ }
+ }
+ throw new CatalogApiException(ErrorCode.CAT_NO_PRICE_FOR_CURRENCY, currency);
+ }
+
+ protected DefaultInternationalPrice setPrices(final DefaultPrice[] prices) {
+ this.prices = prices;
+ return this;
+ }
+
+
+ @Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ final Currency[] supportedCurrencies = catalog.getCurrentSupportedCurrencies();
+ for (final Price p : prices) {
+ final Currency currency = p.getCurrency();
+ if (!currencyIsSupported(currency, supportedCurrencies)) {
+ errors.add("Unsupported currency: " + currency, catalog.getCatalogURI(), this.getClass(), "");
+ }
+ try {
+ if (p.getValue().doubleValue() < 0.0) {
+ errors.add("Negative value for price in currency: " + currency, catalog.getCatalogURI(), this.getClass(), "");
+ }
+ } catch (CurrencyValueNull e) {
+ // No currency => nothing to check, ignore exception
+ }
+ }
+ return errors;
+ }
+
+ private boolean currencyIsSupported(final Currency currency, final Currency[] supportedCurrencies) {
+ for (final Currency c : supportedCurrencies) {
+ if (c == currency) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ @Override
+ public void initialize(final StandaloneCatalog root, final URI uri) {
+ if (prices == null) {
+ prices = getZeroPrice(root);
+ }
+ super.initialize(root, uri);
+ }
+
+ private synchronized DefaultPrice[] getZeroPrice(final StandaloneCatalog root) {
+ final Currency[] currencies = root.getCurrentSupportedCurrencies();
+ final DefaultPrice[] zeroPrice = new DefaultPrice[currencies.length];
+ for (int i = 0; i < currencies.length; i++) {
+ zeroPrice[i] = new DefaultPrice();
+ zeroPrice[i].setCurrency(currencies[i]);
+ zeroPrice[i].setValue(new BigDecimal(0));
+ }
+
+ return zeroPrice;
+ }
+
+ @Override
+ public boolean isZero() {
+ for (final DefaultPrice price : prices) {
+ try {
+ if (price.getValue().compareTo(BigDecimal.ZERO) != 0) {
+ return false;
+ }
+ } catch (CurrencyValueNull e) {
+ //Ignore if the currency is null we treat it as 0
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java
new file mode 100644
index 0000000..cc7ee58
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlIDREF;
+
+import org.killbill.billing.catalog.api.Limit;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationError;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultLimit extends ValidatingConfig<StandaloneCatalog> implements Limit {
+ @XmlElement(required = true)
+ @XmlIDREF
+ private DefaultUnit unit;
+
+ @XmlElement(required = false)
+ private Double max;
+
+ @XmlElement(required = false)
+ private Double min;
+
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.Limit#getUnit()
+ */
+ @Override
+ public DefaultUnit getUnit() {
+ return unit;
+ }
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.Limit#getMax()
+ */
+ @Override
+ public Double getMax() {
+ return max;
+ }
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.Limit#getMin()
+ */
+ @Override
+ public Double getMin() {
+ return min;
+ }
+
+ @Override
+ public ValidationErrors validate(StandaloneCatalog root, ValidationErrors errors) {
+ if(max == null && min == null) {
+ errors.add(new ValidationError("max and min cannot both be ommitted",root.getCatalogURI(), Limit.class, ""));
+ } else if (max != null && min != null && max.doubleValue() < min.doubleValue()) {
+ errors.add(new ValidationError("max must be greater than min",root.getCatalogURI(), Limit.class, ""));
+ }
+
+ return errors;
+ }
+
+ @Override
+ public boolean compliesWith(double value) {
+ if (max != null) {
+ if (value > max.doubleValue()) {
+ return false;
+ }
+ }
+ if (min != null) {
+ if (value < min.doubleValue()) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultListing.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultListing.java
new file mode 100644
index 0000000..0071ecf
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultListing.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.killbill.billing.catalog.api.Listing;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PriceList;
+
+public class DefaultListing implements Listing {
+ private final Plan plan;
+ private final PriceList priceList;
+
+ public DefaultListing(final Plan plan, final PriceList priceList) {
+ super();
+ this.plan = plan;
+ this.priceList = priceList;
+ }
+
+ @Override
+ public Plan getPlan() {
+ return plan;
+ }
+
+ @Override
+ public PriceList getPriceList() {
+ return priceList;
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPrice.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPrice.java
new file mode 100644
index 0000000..bb3f066
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPrice.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import java.math.BigDecimal;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.CurrencyValueNull;
+import org.killbill.billing.catalog.api.Price;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultPrice extends ValidatingConfig<StandaloneCatalog> implements Price {
+ @XmlElement(required = true)
+ private Currency currency;
+
+ @XmlElement(required = true, nillable = true)
+ private BigDecimal value;
+
+ public DefaultPrice() {
+ // for serialization support
+ }
+
+ public DefaultPrice(final BigDecimal value, final Currency currency) {
+ // for sanity support
+ this.value = value;
+ this.currency = currency;
+ }
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.IPrice#getCurrency()
+ */
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.IPrice#getValue()
+ */
+ @Override
+ public BigDecimal getValue() throws CurrencyValueNull {
+ if (value == null) {
+ throw new CurrencyValueNull(currency);
+ }
+ return value;
+ }
+
+ protected DefaultPrice setCurrency(final Currency currency) {
+ this.currency = currency;
+ return this;
+ }
+
+ protected DefaultPrice setValue(final BigDecimal value) {
+ this.value = value;
+ return this;
+ }
+
+ @Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ return errors;
+
+ }
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java
new file mode 100644
index 0000000..cdfdbff
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlID;
+import javax.xml.bind.annotation.XmlIDREF;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationError;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultPriceList extends ValidatingConfig<StandaloneCatalog> implements PriceList {
+
+ @XmlAttribute(required = true)
+ @XmlID
+ private String name;
+
+ @XmlAttribute(required = false)
+ private Boolean retired = false;
+
+ @XmlElementWrapper(name = "plans", required = true)
+ @XmlIDREF
+ @XmlElement(name = "plan", required = true)
+ private DefaultPlan[] plans;
+
+ public DefaultPriceList() {
+ }
+
+ public DefaultPriceList(final DefaultPlan[] plans, final String name) {
+ this.plans = plans;
+ this.name = name;
+ }
+
+ @Override
+ public DefaultPlan[] getPlans() {
+ return plans;
+ }
+
+ @Override
+ public boolean isRetired() {
+ return retired;
+ }
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.IPriceList#getName()
+ */
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.IPriceList#findPlan(org.killbill.billing.catalog.api.IProduct, org.killbill.billing.catalog.api.BillingPeriod)
+ */
+ @Override
+ public DefaultPlan findPlan(final Product product, final BillingPeriod period) {
+ for (final DefaultPlan cur : getPlans()) {
+ if (cur.getProduct().equals(product) &&
+ (cur.getBillingPeriod() == null || cur.getBillingPeriod().equals(period))) {
+ return cur;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ for (final DefaultPlan cur : getPlans()) {
+ final int numPlans = findNumberOfPlans(cur.getProduct(), cur.getBillingPeriod());
+ if (numPlans > 1) {
+ errors.add(new ValidationError(
+ String.format("There are %d plans in pricelist %s and have the same product/billingPeriod (%s, %s)",
+ numPlans, getName(), cur.getProduct().getName(), cur.getBillingPeriod()), catalog.getCatalogURI(),
+ DefaultPriceListSet.class, getName()));
+ }
+ }
+ return errors;
+ }
+
+ private int findNumberOfPlans(final Product product, final BillingPeriod period) {
+ int count = 0;
+ for (final DefaultPlan cur : getPlans()) {
+ if (cur.getProduct().equals(product) &&
+ (cur.getBillingPeriod() == null || cur.getBillingPeriod().equals(period))) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ protected DefaultPriceList setRetired(final boolean retired) {
+ this.retired = retired;
+ return this;
+ }
+
+ public DefaultPriceList setName(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ public DefaultPriceList setPlans(final DefaultPlan[] plans) {
+ this.plans = plans;
+ return this;
+ }
+
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java
new file mode 100644
index 0000000..f90e569
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationError;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultPriceListSet extends ValidatingConfig<StandaloneCatalog> {
+ @XmlElement(required = true, name = "defaultPriceList")
+ private PriceListDefault defaultPricelist;
+
+ @XmlElement(required = false, name = "childPriceList")
+ private DefaultPriceList[] childPriceLists = new DefaultPriceList[0];
+
+ public DefaultPriceListSet() {
+ if (childPriceLists == null) {
+ childPriceLists = new DefaultPriceList[0];
+ }
+ }
+
+ public DefaultPriceListSet(final PriceListDefault defaultPricelist, final DefaultPriceList[] childPriceLists) {
+ this.defaultPricelist = defaultPricelist;
+ this.childPriceLists = childPriceLists;
+ }
+
+ public DefaultPlan getPlanFrom(final String priceListName, final Product product,
+ final BillingPeriod period) throws CatalogApiException {
+ DefaultPlan result = null;
+ final DefaultPriceList pl = findPriceListFrom(priceListName);
+ if (pl != null) {
+ result = pl.findPlan(product, period);
+ }
+ if (result != null) {
+ return result;
+ }
+
+ return defaultPricelist.findPlan(product, period);
+ }
+
+ public DefaultPriceList findPriceListFrom(final String priceListName) throws CatalogApiException {
+ if (priceListName == null) {
+ throw new CatalogApiException(ErrorCode.CAT_NULL_PRICE_LIST_NAME);
+ }
+ if (defaultPricelist.getName().equals(priceListName)) {
+ return defaultPricelist;
+ }
+ for (final DefaultPriceList pl : childPriceLists) {
+ if (pl.getName().equals(priceListName)) {
+ return pl;
+ }
+ }
+ throw new CatalogApiException(ErrorCode.CAT_PRICE_LIST_NOT_FOUND, priceListName);
+ }
+
+ @Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ defaultPricelist.validate(catalog, errors);
+ //Check that the default pricelist name is not in use in the children
+ for (final DefaultPriceList pl : childPriceLists) {
+ if (pl.getName().equals(PriceListSet.DEFAULT_PRICELIST_NAME)) {
+ errors.add(new ValidationError("Pricelists cannot use the reserved name '" + PriceListSet.DEFAULT_PRICELIST_NAME + "'",
+ catalog.getCatalogURI(), DefaultPriceListSet.class, pl.getName()));
+ }
+ pl.validate(catalog, errors); // and validate the individual pricelists
+ }
+ return errors;
+ }
+
+ public DefaultPriceList getDefaultPricelist() {
+ return defaultPricelist;
+ }
+
+ public DefaultPriceList[] getChildPriceLists() {
+ return childPriceLists;
+ }
+
+ public List<PriceList> getAllPriceLists() {
+ final List<PriceList> result = new ArrayList<PriceList>(childPriceLists.length + 1);
+ result.add(getDefaultPricelist());
+ for (final PriceList list : getChildPriceLists()) {
+ result.add(list);
+ }
+ return result;
+ }
+
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultUnit.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultUnit.java
new file mode 100644
index 0000000..c383024
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultUnit.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlID;
+
+import org.killbill.billing.catalog.api.Unit;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultUnit extends ValidatingConfig<StandaloneCatalog> implements Unit {
+
+ @XmlAttribute(required = true)
+ @XmlID
+ private String name;
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.Unit#getName()
+ */
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public ValidationErrors validate(StandaloneCatalog root, ValidationErrors errors) {
+ return errors;
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/glue/CatalogModule.java b/catalog/src/main/java/org/killbill/billing/catalog/glue/CatalogModule.java
new file mode 100644
index 0000000..4312cba
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/glue/CatalogModule.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.catalog.DefaultCatalogService;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.CatalogUserApi;
+import org.killbill.billing.catalog.api.user.DefaultCatalogUserApi;
+import org.killbill.billing.catalog.io.ICatalogLoader;
+import org.killbill.billing.catalog.io.VersionedCatalogLoader;
+import org.killbill.billing.util.config.CatalogConfig;
+
+import com.google.inject.AbstractModule;
+
+public class CatalogModule extends AbstractModule {
+
+ protected final ConfigSource configSource;
+
+ public CatalogModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ protected void installConfig() {
+ final CatalogConfig config = new ConfigurationObjectFactory(configSource).build(CatalogConfig.class);
+ bind(CatalogConfig.class).toInstance(config);
+ }
+
+ protected void installCatalog() {
+ bind(CatalogService.class).to(DefaultCatalogService.class).asEagerSingleton();
+ bind(ICatalogLoader.class).to(VersionedCatalogLoader.class).asEagerSingleton();
+ }
+
+ protected void installCatalogUserApi() {
+ bind(CatalogUserApi.class).to(DefaultCatalogUserApi.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ installConfig();
+ installCatalog();
+ installCatalogUserApi();
+ }
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/io/ICatalogLoader.java b/catalog/src/main/java/org/killbill/billing/catalog/io/ICatalogLoader.java
new file mode 100644
index 0000000..33ed459
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/io/ICatalogLoader.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.io;
+
+import org.killbill.billing.catalog.VersionedCatalog;
+import org.killbill.billing.lifecycle.KillbillService.ServiceException;
+
+public interface ICatalogLoader {
+
+ public abstract VersionedCatalog load(String urlString)
+ throws ServiceException;
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/LoadCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/LoadCatalog.java
new file mode 100644
index 0000000..6deb26f
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/LoadCatalog.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import java.io.File;
+
+import org.killbill.billing.util.config.catalog.XMLLoader;
+
+public class LoadCatalog {
+ public static void main(final String[] args) throws Exception {
+ if (args.length != 1) {
+ System.err.println("Usage: <catalog filepath>");
+ System.exit(0);
+ }
+ File file = new File(args[0]);
+ if(!file.exists()) {
+ System.err.println("Error: '" + args[0] + "' does not exist");
+ }
+ StandaloneCatalog catalog = XMLLoader.getObjectFromUri(file.toURI(), StandaloneCatalog.class);
+ if (catalog != null) {
+ System.out.println("Success: Catalog loads!");
+ }
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/PriceListDefault.java b/catalog/src/main/java/org/killbill/billing/catalog/PriceListDefault.java
new file mode 100644
index 0000000..ba2359f
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/PriceListDefault.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.util.config.catalog.ValidationError;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class PriceListDefault extends DefaultPriceList {
+
+ public PriceListDefault() {
+ }
+
+ public PriceListDefault(final DefaultPlan[] defaultPlans) {
+ super(defaultPlans, PriceListSet.DEFAULT_PRICELIST_NAME);
+ }
+
+ @Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ super.validate(catalog, errors);
+ if (!getName().equals(PriceListSet.DEFAULT_PRICELIST_NAME)) {
+ errors.add(new ValidationError("The name of the default pricelist must be 'DEFAULT'",
+ catalog.getCatalogURI(), DefaultPriceList.class, getName()));
+
+ }
+ return errors;
+ }
+
+ @Override
+ public String getName() {
+ return PriceListSet.DEFAULT_PRICELIST_NAME;
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/Case.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/Case.java
new file mode 100644
index 0000000..0b6ed2b
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/Case.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+
+import org.killbill.billing.catalog.DefaultPriceList;
+import org.killbill.billing.catalog.DefaultProduct;
+import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PlanSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+public abstract class Case<T> extends ValidatingConfig<StandaloneCatalog> {
+
+ protected abstract T getResult();
+
+ public abstract DefaultProduct getProduct();
+
+ public abstract ProductCategory getProductCategory();
+
+ public abstract BillingPeriod getBillingPeriod();
+
+ public abstract DefaultPriceList getPriceList();
+
+ public T getResult(final PlanSpecifier planPhase, final StandaloneCatalog c) throws CatalogApiException {
+ if (satisfiesCase(planPhase, c)) {
+ return getResult();
+ }
+ return null;
+ }
+
+ protected boolean satisfiesCase(final PlanSpecifier planPhase, final StandaloneCatalog c) throws CatalogApiException {
+ return (getProduct() == null || getProduct().equals(c.findCurrentProduct(planPhase.getProductName()))) &&
+ (getProductCategory() == null || getProductCategory().equals(planPhase.getProductCategory())) &&
+ (getBillingPeriod() == null || getBillingPeriod().equals(planPhase.getBillingPeriod())) &&
+ (getPriceList() == null || getPriceList().equals(c.findCurrentPriceList(planPhase.getPriceListName())));
+ }
+
+ public static <K> K getResult(final Case<K>[] cases, final PlanSpecifier planSpec, final StandaloneCatalog catalog) throws CatalogApiException {
+ if (cases != null) {
+ for (final Case<K> c : cases) {
+ final K result = c.getResult(planSpec, catalog);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ return null;
+
+ }
+
+ @Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ return errors;
+ }
+
+ protected abstract Case<T> setProduct(DefaultProduct product);
+
+ protected abstract Case<T> setProductCategory(ProductCategory productCategory);
+
+ protected abstract Case<T> setBillingPeriod(BillingPeriod billingPeriod);
+
+ protected abstract Case<T> setPriceList(DefaultPriceList priceList);
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseBillingAlignment.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseBillingAlignment.java
new file mode 100644
index 0000000..9955e83
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseBillingAlignment.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import javax.xml.bind.annotation.XmlElement;
+
+import org.killbill.billing.catalog.api.BillingAlignment;
+
+public class CaseBillingAlignment extends CasePhase<BillingAlignment> {
+
+ @XmlElement(required = true)
+ private BillingAlignment alignment;
+
+ @Override
+ protected BillingAlignment getResult() {
+ return alignment;
+ }
+
+ protected CaseBillingAlignment setAlignment(final BillingAlignment alignment) {
+ this.alignment = alignment;
+ return this;
+ }
+
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseCancelPolicy.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseCancelPolicy.java
new file mode 100644
index 0000000..cd3606c
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseCancelPolicy.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import javax.xml.bind.annotation.XmlElement;
+
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+
+public class CaseCancelPolicy extends CasePhase<BillingActionPolicy> {
+
+ @XmlElement(required = true)
+ private BillingActionPolicy policy;
+
+ @Override
+ protected BillingActionPolicy getResult() {
+ return policy;
+ }
+
+ protected CaseCancelPolicy setPolicy(final BillingActionPolicy policy) {
+ this.policy = policy;
+ return this;
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseChange.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseChange.java
new file mode 100644
index 0000000..5812ae2
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseChange.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlIDREF;
+
+import org.killbill.billing.catalog.DefaultPriceList;
+import org.killbill.billing.catalog.DefaultProduct;
+import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PhaseType;
+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.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public abstract class CaseChange<T> extends ValidatingConfig<StandaloneCatalog> {
+
+ @XmlElement(required = false)
+ private PhaseType phaseType;
+
+ @XmlElement(required = false)
+ @XmlIDREF
+ private DefaultProduct fromProduct;
+
+ @XmlElement(required = false)
+ private ProductCategory fromProductCategory;
+
+ @XmlElement(required = false)
+ private BillingPeriod fromBillingPeriod;
+
+ @XmlElement(required = false)
+ @XmlIDREF
+ private DefaultPriceList fromPriceList;
+
+ @XmlElement(required = false)
+ @XmlIDREF
+ private DefaultProduct toProduct;
+
+ @XmlElement(required = false)
+ private ProductCategory toProductCategory;
+
+ @XmlElement(required = false)
+ private BillingPeriod toBillingPeriod;
+
+ @XmlElement(required = false)
+ @XmlIDREF
+ private DefaultPriceList toPriceList;
+
+ protected abstract T getResult();
+
+ public T getResult(final PlanPhaseSpecifier from,
+ final PlanSpecifier to, final StandaloneCatalog catalog) throws CatalogApiException {
+ if (
+ (phaseType == null || from.getPhaseType() == phaseType) &&
+ (fromProduct == null || fromProduct.equals(catalog.findCurrentProduct(from.getProductName()))) &&
+ (fromProductCategory == null || fromProductCategory.equals(from.getProductCategory())) &&
+ (fromBillingPeriod == null || fromBillingPeriod.equals(from.getBillingPeriod())) &&
+ (toProduct == null || toProduct.equals(catalog.findCurrentProduct(to.getProductName()))) &&
+ (toProductCategory == null || toProductCategory.equals(to.getProductCategory())) &&
+ (toBillingPeriod == null || toBillingPeriod.equals(to.getBillingPeriod())) &&
+ (fromPriceList == null || fromPriceList.equals(catalog.findCurrentPriceList(from.getPriceListName()))) &&
+ (toPriceList == null || toPriceList.equals(catalog.findCurrentPriceList(to.getPriceListName())))
+ ) {
+ return getResult();
+ }
+ return null;
+ }
+
+ public static <K> K getResult(final CaseChange<K>[] cases, final PlanPhaseSpecifier from,
+ final PlanSpecifier to, final StandaloneCatalog catalog) throws CatalogApiException {
+ if (cases != null) {
+ for (final CaseChange<K> cc : cases) {
+ final K result = cc.getResult(from, to, catalog);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ return null;
+
+ }
+
+ @Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ return errors;
+ }
+
+ protected CaseChange<T> setPhaseType(final PhaseType phaseType) {
+ this.phaseType = phaseType;
+ return this;
+ }
+
+ protected CaseChange<T> setFromProduct(final DefaultProduct fromProduct) {
+ this.fromProduct = fromProduct;
+ return this;
+ }
+
+ protected CaseChange<T> setFromProductCategory(final ProductCategory fromProductCategory) {
+ this.fromProductCategory = fromProductCategory;
+ return this;
+ }
+
+ protected CaseChange<T> setFromBillingPeriod(final BillingPeriod fromBillingPeriod) {
+ this.fromBillingPeriod = fromBillingPeriod;
+ return this;
+ }
+
+ protected CaseChange<T> setFromPriceList(final DefaultPriceList fromPriceList) {
+ this.fromPriceList = fromPriceList;
+ return this;
+ }
+
+ protected CaseChange<T> setToProduct(final DefaultProduct toProduct) {
+ this.toProduct = toProduct;
+ return this;
+ }
+
+ protected CaseChange<T> setToProductCategory(final ProductCategory toProductCategory) {
+ this.toProductCategory = toProductCategory;
+ return this;
+ }
+
+ protected CaseChange<T> setToBillingPeriod(final BillingPeriod toBillingPeriod) {
+ this.toBillingPeriod = toBillingPeriod;
+ return this;
+ }
+
+ protected CaseChange<T> setToPriceList(final DefaultPriceList toPriceList) {
+ this.toPriceList = toPriceList;
+ return this;
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseChangePlanAlignment.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseChangePlanAlignment.java
new file mode 100644
index 0000000..5a9ea7f
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseChangePlanAlignment.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import javax.xml.bind.annotation.XmlElement;
+
+import org.killbill.billing.catalog.api.PlanAlignmentChange;
+
+public class CaseChangePlanAlignment extends CaseChange<PlanAlignmentChange> {
+
+ @XmlElement(required = true)
+ private PlanAlignmentChange alignment;
+
+ @Override
+ protected PlanAlignmentChange getResult() {
+ return alignment;
+ }
+
+ protected CaseChangePlanAlignment setAlignment(final PlanAlignmentChange alignment) {
+ this.alignment = alignment;
+ return this;
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseChangePlanPolicy.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseChangePlanPolicy.java
new file mode 100644
index 0000000..e75b7bf
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseChangePlanPolicy.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlSeeAlso;
+
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+
+@XmlSeeAlso(CaseChange.class)
+public class CaseChangePlanPolicy extends CaseChange<BillingActionPolicy> {
+
+ @XmlElement(required = true)
+ private BillingActionPolicy policy;
+
+ @Override
+ protected BillingActionPolicy getResult() {
+ return policy;
+ }
+
+ protected CaseChangePlanPolicy setPolicy(final BillingActionPolicy policy) {
+ this.policy = policy;
+ return this;
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseCreateAlignment.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseCreateAlignment.java
new file mode 100644
index 0000000..400dc1c
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseCreateAlignment.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import javax.xml.bind.annotation.XmlElement;
+
+import org.killbill.billing.catalog.api.PlanAlignmentCreate;
+
+public class CaseCreateAlignment extends CaseStandardNaming<PlanAlignmentCreate> {
+
+ @XmlElement(required = true)
+ private PlanAlignmentCreate alignment;
+
+ @Override
+ protected PlanAlignmentCreate getResult() {
+ return alignment;
+ }
+
+ protected CaseCreateAlignment setAlignment(final PlanAlignmentCreate alignment) {
+ this.alignment = alignment;
+ return this;
+ }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/CasePhase.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/CasePhase.java
new file mode 100644
index 0000000..c2e2121
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/CasePhase.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import javax.xml.bind.annotation.XmlElement;
+
+import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PlanSpecifier;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+public abstract class CasePhase<T> extends CaseStandardNaming<T> {
+
+ @XmlElement(required = false)
+ private PhaseType phaseType;
+
+ public T getResult(final PlanPhaseSpecifier specifier, final StandaloneCatalog c) throws CatalogApiException {
+ if ((phaseType == null || specifier.getPhaseType() == phaseType)
+ && satisfiesCase(new PlanSpecifier(specifier), c)
+ ) {
+ return getResult();
+ }
+ return null;
+ }
+
+ public static <K> K getResult(final CasePhase<K>[] cases, final PlanPhaseSpecifier planSpec, final StandaloneCatalog catalog) throws CatalogApiException {
+ if (cases != null) {
+ for (final CasePhase<K> cp : cases) {
+ final K result = cp.getResult(planSpec, catalog);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ return null;
+
+ }
+
+ @Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ return errors;
+ }
+
+ protected CasePhase<T> setPhaseType(final PhaseType phaseType) {
+ this.phaseType = phaseType;
+ return this;
+ }
+
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/CasePriceList.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/CasePriceList.java
new file mode 100644
index 0000000..7756ff2
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/CasePriceList.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlIDREF;
+
+import org.killbill.billing.catalog.DefaultPriceList;
+import org.killbill.billing.catalog.DefaultProduct;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+
+public class CasePriceList extends Case<DefaultPriceList> {
+ @XmlElement(required = false, name = "fromProduct")
+ @XmlIDREF
+ private DefaultProduct fromProduct;
+
+ @XmlElement(required = false, name = "fromProductCategory")
+ private ProductCategory fromProductCategory;
+
+ @XmlElement(required = false, name = "fromBillingPeriod")
+ private BillingPeriod fromBillingPeriod;
+
+ @XmlElement(required = false, name = "fromPriceList")
+ @XmlIDREF
+ private DefaultPriceList fromPriceList;
+
+ @XmlElement(required = true, name = "toPriceList")
+ @XmlIDREF
+ private DefaultPriceList toPriceList;
+
+ public DefaultProduct getProduct() {
+ return fromProduct;
+ }
+
+ public ProductCategory getProductCategory() {
+ return fromProductCategory;
+ }
+
+ public BillingPeriod getBillingPeriod() {
+ return fromBillingPeriod;
+ }
+
+ public DefaultPriceList getPriceList() {
+ return fromPriceList;
+ }
+
+ protected DefaultPriceList getResult() {
+ return toPriceList;
+ }
+
+ protected CasePriceList setProduct(final DefaultProduct product) {
+ this.fromProduct = product;
+ return this;
+ }
+
+ protected CasePriceList setProductCategory(final ProductCategory productCategory) {
+ this.fromProductCategory = productCategory;
+ return this;
+ }
+
+ protected CasePriceList setBillingPeriod(final BillingPeriod billingPeriod) {
+ this.fromBillingPeriod = billingPeriod;
+ return this;
+ }
+
+ protected CasePriceList setPriceList(final DefaultPriceList priceList) {
+ this.fromPriceList = priceList;
+ return this;
+ }
+
+
+ protected CasePriceList setToPriceList(final DefaultPriceList toPriceList) {
+ this.toPriceList = toPriceList;
+ return this;
+ }
+
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseStandardNaming.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseStandardNaming.java
new file mode 100644
index 0000000..31d8e0e
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/CaseStandardNaming.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlIDREF;
+
+import org.killbill.billing.catalog.DefaultPriceList;
+import org.killbill.billing.catalog.DefaultProduct;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+
+public abstract class CaseStandardNaming<T> extends Case<T> {
+ @XmlElement(required = false, name = "product")
+ @XmlIDREF
+ private DefaultProduct product;
+ @XmlElement(required = false, name = "productCategory")
+ private ProductCategory productCategory;
+
+ @XmlElement(required = false, name = "billingPeriod")
+ private BillingPeriod billingPeriod;
+
+ @XmlElement(required = false, name = "priceList")
+ @XmlIDREF
+ private DefaultPriceList priceList;
+
+ public DefaultProduct getProduct() {
+ return product;
+ }
+
+ public ProductCategory getProductCategory() {
+ return productCategory;
+ }
+
+ public BillingPeriod getBillingPeriod() {
+ return billingPeriod;
+ }
+
+ public DefaultPriceList getPriceList() {
+ return priceList;
+ }
+
+ protected CaseStandardNaming<T> setProduct(final DefaultProduct product) {
+ this.product = product;
+ return this;
+ }
+
+ protected CaseStandardNaming<T> setProductCategory(final ProductCategory productCategory) {
+ this.productCategory = productCategory;
+ return this;
+ }
+
+ protected CaseStandardNaming<T> setBillingPeriod(final BillingPeriod billingPeriod) {
+ this.billingPeriod = billingPeriod;
+ return this;
+ }
+
+ protected CaseStandardNaming<T> setPriceList(final DefaultPriceList priceList) {
+ this.priceList = priceList;
+ return this;
+ }
+
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/CatalogTestSuiteNoDB.java b/catalog/src/test/java/org/killbill/billing/catalog/CatalogTestSuiteNoDB.java
new file mode 100644
index 0000000..c3d724f
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/CatalogTestSuiteNoDB.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.testng.annotations.BeforeClass;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.billing.catalog.glue.TestCatalogModuleNoDB;
+import org.killbill.billing.catalog.io.VersionedCatalogLoader;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public abstract class CatalogTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ @Inject
+ protected VersionedCatalogLoader loader;
+
+ @BeforeClass(groups = "fast")
+ protected void beforeClass() throws Exception {
+ final Injector injector = Guice.createInjector(new TestCatalogModuleNoDB(configSource));
+ injector.injectMembers(this);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/glue/TestCatalogModule.java b/catalog/src/test/java/org/killbill/billing/catalog/glue/TestCatalogModule.java
new file mode 100644
index 0000000..77e157c
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/glue/TestCatalogModule.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+
+public class TestCatalogModule extends CatalogModule {
+
+ public TestCatalogModule(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ public void configure() {
+ super.configure();
+ install(new GuicyKillbillTestNoDBModule());
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/glue/TestCatalogModuleNoDB.java b/catalog/src/test/java/org/killbill/billing/catalog/glue/TestCatalogModuleNoDB.java
new file mode 100644
index 0000000..269271e
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/glue/TestCatalogModuleNoDB.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.glue;
+
+import org.skife.config.ConfigSource;
+
+public class TestCatalogModuleNoDB extends TestCatalogModule {
+
+ public TestCatalogModuleNoDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java b/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java
new file mode 100644
index 0000000..77c3abe
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.io;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.xml.bind.JAXBException;
+import javax.xml.transform.TransformerException;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import org.xml.sax.SAXException;
+
+import org.killbill.billing.catalog.CatalogTestSuiteNoDB;
+import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.catalog.VersionedCatalog;
+import org.killbill.billing.catalog.api.InvalidConfigException;
+import org.killbill.billing.lifecycle.KillbillService.ServiceException;
+
+import com.google.common.io.Resources;
+
+public class TestVersionedCatalogLoader extends CatalogTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testAppendToURI() throws IOException, URISyntaxException {
+ final URL u1 = new URL("http://www.ning.com/foo");
+ Assert.assertEquals(loader.appendToURI(u1, "bar").toString(), "http://www.ning.com/foo/bar");
+
+ final URL u2 = new URL("http://www.ning.com/foo/");
+ Assert.assertEquals(loader.appendToURI(u2, "bar").toString(), "http://www.ning.com/foo/bar");
+ }
+
+ @Test(groups = "fast")
+ public void testFindXmlFileReferences() throws MalformedURLException, URISyntaxException {
+ final String page = "dg.xml\n" +
+ "replica.foo\n" +
+ "snv1/\n" +
+ "viking.xml\n";
+ final List<URI> urls = loader.findXmlFileReferences(page, new URL("http://ning.com/"));
+ Assert.assertEquals(urls.size(), 2);
+ Assert.assertEquals(urls.get(0).toString(), "http://ning.com/dg.xml");
+ Assert.assertEquals(urls.get(1).toString(), "http://ning.com/viking.xml");
+ }
+
+ @Test(groups = "fast")
+ public void testExtractHrefs() {
+ final String page = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">" +
+ "<html>" +
+ " <head>" +
+ " <title>Index of /config/trunk/xno</title>" +
+ " </head>" +
+ " <body>" +
+ "<h1>Index of /config/trunk/xno</h1>" +
+ "<ul><li><a href=\"/config/trunk/\"> Parent Directory</a></li>" +
+ "<li><a href=\"dg.xml\"> dg.xml</a></li>" +
+ "<li><a href=\"replica.foo\"> replica/</a></li>" +
+ "<li><a href=\"replica2/\"> replica2/</a></li>" +
+ "<li><a href=\"replica_dyson/\"> replica_dyson/</a></li>" +
+ "<li><a href=\"snv1/\"> snv1/</a></li>" +
+ "<li><a href=\"viking.xml\"> viking.xml</a></li>" +
+ "</ul>" +
+ "<address>Apache/2.2.3 (CentOS) Server at <a href=\"mailto:kate@ning.com\">gepo.ningops.net</a> Port 80</address>" +
+ "</body></html>";
+ final List<String> hrefs = loader.extractHrefs(page);
+ Assert.assertEquals(hrefs.size(), 8);
+ Assert.assertEquals(hrefs.get(0), "/config/trunk/");
+ Assert.assertEquals(hrefs.get(1), "dg.xml");
+ }
+
+ @Test(groups = "fast")
+ public void testFindXmlUrlReferences() throws MalformedURLException, URISyntaxException {
+ final String page = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">" +
+ "<html>" +
+ " <head>" +
+ " <title>Index of /config/trunk/xno</title>" +
+ " </head>" +
+ " <body>" +
+ "<h1>Index of /config/trunk/xno</h1>" +
+ "<ul><li><a href=\"/config/trunk/\"> Parent Directory</a></li>" +
+ "<li><a href=\"dg.xml\"> dg.xml</a></li>" +
+ "<li><a href=\"replica.foo\"> replica/</a></li>" +
+ "<li><a href=\"replica2/\"> replica2/</a></li>" +
+ "<li><a href=\"replica_dyson/\"> replica_dyson/</a></li>" +
+ "<li><a href=\"snv1/\"> snv1/</a></li>" +
+ "<li><a href=\"viking.xml\"> viking.xml</a></li>" +
+ "</ul>" +
+ "<address>Apache/2.2.3 (CentOS) Server at <a href=\"mailto:kate@ning.com\">gepo.ningops.net</a> Port 80</address>" +
+ "</body></html>";
+ final List<URI> uris = loader.findXmlUrlReferences(page, new URL("http://ning.com/"));
+ Assert.assertEquals(uris.size(), 2);
+ Assert.assertEquals(uris.get(0).toString(), "http://ning.com/dg.xml");
+ Assert.assertEquals(uris.get(1).toString(), "http://ning.com/viking.xml");
+ }
+
+ @Test(groups = "fast")
+ public void testLoad() throws IOException, SAXException, InvalidConfigException, JAXBException, TransformerException, URISyntaxException, ServiceException {
+ final VersionedCatalog c = loader.load(Resources.getResource("versionedCatalog").toString());
+ Assert.assertEquals(c.size(), 3);
+ final Iterator<StandaloneCatalog> it = c.iterator();
+ DateTime dt = new DateTime("2011-01-01T00:00:00+00:00");
+ Assert.assertEquals(it.next().getEffectiveDate(), dt.toDate());
+ dt = new DateTime("2011-02-02T00:00:00+00:00");
+ Assert.assertEquals(it.next().getEffectiveDate(), dt.toDate());
+ dt = new DateTime("2011-03-03T00:00:00+00:00");
+ Assert.assertEquals(it.next().getEffectiveDate(), dt.toDate());
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLReader.java b/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLReader.java
new file mode 100644
index 0000000..0b906af
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLReader.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.io;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.io.Resources;
+import org.killbill.billing.catalog.CatalogTestSuiteNoDB;
+import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+
+public class TestXMLReader extends CatalogTestSuiteNoDB {
+ @Test(groups = "fast")
+ public void testCatalogLoad() {
+ try {
+ XMLLoader.getObjectFromString(Resources.getResource("WeaponsHire.xml").toExternalForm(), StandaloneCatalog.class);
+ XMLLoader.getObjectFromString(Resources.getResource("WeaponsHireSmall.xml").toExternalForm(), StandaloneCatalog.class);
+ XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
+ } catch (Exception e) {
+ Assert.fail(e.toString());
+ }
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockCatalogModule.java b/catalog/src/test/java/org/killbill/billing/catalog/MockCatalogModule.java
new file mode 100644
index 0000000..f2585a1
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockCatalogModule.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.mockito.Mockito;
+
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogService;
+
+import com.google.inject.AbstractModule;
+
+public class MockCatalogModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ final Catalog catalog = Mockito.mock(Catalog.class);
+
+ final CatalogService catalogService = Mockito.mock(CatalogService.class);
+ Mockito.when(catalogService.getCurrentCatalog()).thenReturn(new MockCatalog());
+ Mockito.when(catalogService.getFullCatalog()).thenReturn(catalog);
+ bind(CatalogService.class).toInstance(catalogService);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockCatalogService.java b/catalog/src/test/java/org/killbill/billing/catalog/MockCatalogService.java
new file mode 100644
index 0000000..bf38751
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockCatalogService.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.StaticCatalog;
+
+public class MockCatalogService extends DefaultCatalogService {
+
+ private final MockCatalog catalog;
+
+ public MockCatalogService(final MockCatalog catalog) {
+ super(null, null);
+ this.catalog = catalog;
+ }
+
+ @Override
+ public synchronized void loadCatalog() throws ServiceException {
+ }
+
+ @Override
+ public String getName() {
+ return "Mock Catalog";
+ }
+
+ @Override
+ public Catalog getFullCatalog() {
+ return catalog;
+ }
+
+ @Override
+ public Catalog get() {
+ return catalog;
+ }
+
+ @Override
+ public StaticCatalog getCurrentCatalog() {
+ return catalog;
+ }
+
+
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockInternationalPrice.java b/catalog/src/test/java/org/killbill/billing/catalog/MockInternationalPrice.java
new file mode 100644
index 0000000..1500ce1
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockInternationalPrice.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import java.math.BigDecimal;
+
+import org.killbill.billing.catalog.api.Currency;
+
+public class MockInternationalPrice extends DefaultInternationalPrice {
+
+ public static MockInternationalPrice create0USD() {
+ return new MockInternationalPrice(new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.ZERO));
+ }
+
+ public static MockInternationalPrice create1USD() {
+ return new MockInternationalPrice(new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.ONE));
+ }
+
+ public static MockInternationalPrice createUSD(final String value) {
+ return new MockInternationalPrice(new DefaultPrice().setCurrency(Currency.USD).setValue(new BigDecimal(value)));
+ }
+
+ public MockInternationalPrice(final DefaultPrice... price) {
+ setPrices(price);
+ }
+
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java b/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java
new file mode 100644
index 0000000..e77c761
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+public class MockPlan extends DefaultPlan {
+
+ public static MockPlan createBicycleTrialEvergreen1USD(final int trialDurationInDays) {
+ return new MockPlan("BicycleTrialEvergreen1USD",
+ MockProduct.createBicycle(),
+ new DefaultPlanPhase[]{MockPlanPhase.createTrial(trialDurationInDays)},
+ MockPlanPhase.create1USDMonthlyEvergreen(),
+ -1);
+ }
+
+ public static MockPlan createBicycleTrialEvergreen1USD() {
+ return new MockPlan("BicycleTrialEvergreen1USD",
+ MockProduct.createBicycle(),
+ new DefaultPlanPhase[]{MockPlanPhase.create30DayTrial()},
+ MockPlanPhase.create1USDMonthlyEvergreen(),
+ -1);
+ }
+
+ public static MockPlan createSportsCarTrialEvergreen100USD() {
+ return new MockPlan("SportsCarTrialEvergreen100USD",
+ MockProduct.createSportsCar(),
+ new DefaultPlanPhase[]{MockPlanPhase.create30DayTrial()},
+ MockPlanPhase.createUSDMonthlyEvergreen("100.00", null),
+ -1);
+ }
+
+ public static MockPlan createPickupTrialEvergreen10USD() {
+ return new MockPlan("PickupTrialEvergreen10USD",
+ MockProduct.createPickup(),
+ new DefaultPlanPhase[]{MockPlanPhase.create30DayTrial()},
+ MockPlanPhase.createUSDMonthlyEvergreen("10.00", null),
+ -1);
+ }
+
+ public static MockPlan createJetTrialEvergreen1000USD() {
+ return new MockPlan("JetTrialEvergreen1000USD",
+ MockProduct.createJet(),
+ new DefaultPlanPhase[]{MockPlanPhase.create30DayTrial()},
+ MockPlanPhase.create1USDMonthlyEvergreen(),
+ -1);
+ }
+
+ public static MockPlan createJetTrialFixedTermEvergreen1000USD() {
+ return new MockPlan("JetTrialEvergreen1000USD",
+ MockProduct.createJet(),
+ new DefaultPlanPhase[]{MockPlanPhase.create30DayTrial(), MockPlanPhase.createUSDMonthlyFixedTerm("500.00", null, 6)},
+ MockPlanPhase.create1USDMonthlyEvergreen(),
+ -1);
+ }
+
+ public static MockPlan createHornMonthlyNoTrial1USD() {
+ return new MockPlan("Horn1USD",
+ MockProduct.createHorn(),
+ new DefaultPlanPhase[]{},
+ MockPlanPhase.create1USDMonthlyEvergreen(),
+ -1);
+ }
+
+ public MockPlan() {
+ this("BicycleTrialEvergreen1USD",
+ MockProduct.createBicycle(),
+ new DefaultPlanPhase[]{MockPlanPhase.create30DayTrial()},
+ MockPlanPhase.create1USDMonthlyEvergreen(),
+ -1);
+ }
+
+ public MockPlan(final String name, final DefaultProduct product, final DefaultPlanPhase[] planPhases, final DefaultPlanPhase finalPhase, final int plansAllowedInBundle) {
+ setName(name);
+ setProduct(product);
+ setFinalPhase(finalPhase);
+ setInitialPhases(planPhases);
+ setPlansAllowedInBundle(plansAllowedInBundle);
+
+ finalPhase.setPlan(this);
+ for (final DefaultPlanPhase pp : planPhases) {
+ pp.setPlan(this);
+ }
+ }
+
+ public static MockPlan createBicycleNoTrialEvergreen1USD() {
+ return new MockPlan("BicycleNoTrialEvergreen1USD",
+ MockProduct.createBicycle(),
+ new DefaultPlanPhase[]{},
+ MockPlanPhase.createUSDMonthlyEvergreen("1.0", null),
+ -1);
+ }
+
+ public MockPlan(final MockPlanPhase mockPlanPhase) {
+ setName("Test");
+ setProduct(MockProduct.createBicycle());
+ setFinalPhase(mockPlanPhase);
+
+ mockPlanPhase.setPlan(this);
+ }
+
+ public MockPlan(final String planName) {
+ setName(planName);
+ setProduct(new MockProduct());
+ setFinalPhase(new MockPlanPhase(this));
+ setInitialPhases(null);
+ setPlansAllowedInBundle(1);
+ }
+
+ public static DefaultPlan[] createAll() {
+ return new DefaultPlan[]{
+ createBicycleTrialEvergreen1USD(),
+ createBicycleNoTrialEvergreen1USD(),
+ createPickupTrialEvergreen10USD(),
+ createSportsCarTrialEvergreen100USD(),
+ createJetTrialEvergreen1000USD(),
+ createJetTrialFixedTermEvergreen1000USD(),
+ createHornMonthlyNoTrial1USD()
+ };
+ }
+
+
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockPlanPhase.java b/catalog/src/test/java/org/killbill/billing/catalog/MockPlanPhase.java
new file mode 100644
index 0000000..ab9aab6
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockPlanPhase.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import javax.annotation.Nullable;
+
+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.TimeUnit;
+
+public class MockPlanPhase extends DefaultPlanPhase {
+
+ public static MockPlanPhase create1USDMonthlyEvergreen() {
+ return (MockPlanPhase) new MockPlanPhase(BillingPeriod.MONTHLY,
+ PhaseType.EVERGREEN,
+ new DefaultDuration().setUnit(TimeUnit.UNLIMITED),
+ MockInternationalPrice.create1USD(),
+ null).setPlan(MockPlan.createBicycleNoTrialEvergreen1USD());
+ }
+
+ public static MockPlanPhase createUSDMonthlyEvergreen(final String reccuringUSDPrice, final String fixedPrice) {
+ return new MockPlanPhase(BillingPeriod.MONTHLY,
+ PhaseType.EVERGREEN,
+ new DefaultDuration().setUnit(TimeUnit.UNLIMITED),
+ (reccuringUSDPrice == null) ? null : MockInternationalPrice.createUSD(reccuringUSDPrice),
+ (fixedPrice == null) ? null : MockInternationalPrice.createUSD(fixedPrice));
+ }
+
+ public static MockPlanPhase createUSDMonthlyFixedTerm(final String reccuringUSDPrice, final String fixedPrice, final int durationInMonths) {
+ return new MockPlanPhase(BillingPeriod.MONTHLY,
+ PhaseType.FIXEDTERM,
+ new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(durationInMonths),
+ (reccuringUSDPrice == null) ? null : MockInternationalPrice.createUSD(reccuringUSDPrice),
+ (fixedPrice == null) ? null : MockInternationalPrice.createUSD(fixedPrice));
+ }
+
+ public static MockPlanPhase create30DayTrial() {
+ return createTrial(30);
+ }
+
+ public static MockPlanPhase createTrial(final int days) {
+ return new MockPlanPhase(BillingPeriod.NO_BILLING_PERIOD,
+ PhaseType.TRIAL,
+ new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(days),
+ null,
+ MockInternationalPrice.create1USD()
+ );
+ }
+
+ public MockPlanPhase(
+ final BillingPeriod billingPeriod,
+ final PhaseType type,
+ final DefaultDuration duration,
+ final DefaultInternationalPrice recurringPrice,
+ final DefaultInternationalPrice fixedPrice) {
+ setBillingPeriod(billingPeriod);
+ setPhaseType(type);
+ setDuration(duration);
+ setRecurringPrice(recurringPrice);
+ setFixedPrice(fixedPrice);
+ }
+
+ public MockPlanPhase() {
+ this(new MockInternationalPrice(), null);
+ }
+
+ public MockPlanPhase(@Nullable final MockInternationalPrice recurringPrice,
+ @Nullable final MockInternationalPrice fixedPrice) {
+ this(recurringPrice, fixedPrice, BillingPeriod.MONTHLY);
+ }
+
+ public MockPlanPhase(@Nullable final MockInternationalPrice recurringPrice,
+ @Nullable final MockInternationalPrice fixedPrice,
+ final BillingPeriod billingPeriod) {
+ this(recurringPrice, fixedPrice, billingPeriod, PhaseType.EVERGREEN);
+ }
+
+ public MockPlanPhase(@Nullable final MockInternationalPrice recurringPrice,
+ @Nullable final MockInternationalPrice fixedPrice,
+ final BillingPeriod billingPeriod,
+ final PhaseType phaseType) {
+ setBillingPeriod(billingPeriod);
+ setPhaseType(phaseType);
+ setDuration(new DefaultDuration().setNumber(-1).setUnit(TimeUnit.UNLIMITED));
+ setRecurringPrice(recurringPrice);
+ setFixedPrice(fixedPrice);
+ setPlan(new MockPlan(this));
+ }
+
+ public MockPlanPhase(final MockPlan mockPlan) {
+ setBillingPeriod(BillingPeriod.MONTHLY);
+ setPhaseType(PhaseType.EVERGREEN);
+ setDuration(new DefaultDuration().setNumber(-1).setUnit(TimeUnit.UNLIMITED));
+ setRecurringPrice(new MockInternationalPrice());
+ setFixedPrice(null);
+ setPlan(mockPlan);
+ }
+
+ public MockPlanPhase(final Plan plan, final PhaseType phaseType) {
+ setBillingPeriod(BillingPeriod.MONTHLY);
+ setPhaseType(phaseType);
+ setDuration(new DefaultDuration().setNumber(-1).setUnit(TimeUnit.UNLIMITED));
+ setRecurringPrice(new MockInternationalPrice());
+ setFixedPrice(null);
+ setPlan(plan);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockPriceList.java b/catalog/src/test/java/org/killbill/billing/catalog/MockPriceList.java
new file mode 100644
index 0000000..9937687
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockPriceList.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.killbill.billing.catalog.api.PriceListSet;
+
+public class MockPriceList extends DefaultPriceList {
+
+ public MockPriceList() {
+ setName(PriceListSet.DEFAULT_PRICELIST_NAME);
+ setRetired(false);
+ setPlans(MockPlan.createAll());
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockProduct.java b/catalog/src/test/java/org/killbill/billing/catalog/MockProduct.java
new file mode 100644
index 0000000..f0a718e
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockProduct.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.killbill.billing.catalog.api.ProductCategory;
+
+public class MockProduct extends DefaultProduct {
+
+ public MockProduct() {
+ setName("TestProduct");
+ setCatagory(ProductCategory.BASE);
+ setCatalogName("Vehcles");
+ }
+
+ public MockProduct(final String name, final ProductCategory category, final String catalogName) {
+ setName(name);
+ setCatagory(category);
+ setCatalogName(catalogName);
+ }
+
+ public static MockProduct createBicycle() {
+ return new MockProduct("Bicycle", ProductCategory.BASE, "Vehcles");
+ }
+
+ public static MockProduct createPickup() {
+ return new MockProduct("Pickup", ProductCategory.BASE, "Vehcles");
+ }
+
+ public static MockProduct createSportsCar() {
+ return new MockProduct("SportsCar", ProductCategory.BASE, "Vehcles");
+ }
+
+ public static MockProduct createJet() {
+ return new MockProduct("Jet", ProductCategory.BASE, "Vehcles");
+ }
+
+ public static MockProduct createHorn() {
+ return new MockProduct("Horn", ProductCategory.ADD_ON, "Vehcles");
+ }
+
+ public static MockProduct createSpotlight() {
+ return new MockProduct("spotlight", ProductCategory.ADD_ON, "Vehcles");
+ }
+
+ public static MockProduct createRedPaintJob() {
+ return new MockProduct("RedPaintJob", ProductCategory.ADD_ON, "Vehcles");
+ }
+
+ public static DefaultProduct[] createAll() {
+ return new MockProduct[]{
+ createBicycle(),
+ createPickup(),
+ createSportsCar(),
+ createJet(),
+ createHorn(),
+ createRedPaintJob()
+ };
+ }
+
+
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/rules/Result.java b/catalog/src/test/java/org/killbill/billing/catalog/rules/Result.java
new file mode 100644
index 0000000..526fb23
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/rules/Result.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+enum Result {
+ FOO, BAR, TINKYWINKY, DIPSY, LALA, PO
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/rules/TestLoadRules.java b/catalog/src/test/java/org/killbill/billing/catalog/rules/TestLoadRules.java
new file mode 100644
index 0000000..9c25a3e
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/rules/TestLoadRules.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import java.net.URI;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.CatalogTestSuiteNoDB;
+import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PlanAlignmentCreate;
+import org.killbill.billing.catalog.api.PlanSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+
+import com.google.common.io.Resources;
+
+public class TestLoadRules extends CatalogTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void test() throws Exception {
+ final URI uri = new URI(Resources.getResource("WeaponsHireSmall.xml").toExternalForm());
+ final StandaloneCatalog catalog = XMLLoader.getObjectFromUri(uri, StandaloneCatalog.class);
+ Assert.assertNotNull(catalog);
+ final PlanRules rules = catalog.getPlanRules();
+
+ final PlanSpecifier specifier = new PlanSpecifier("Laser-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY,
+ "DEFAULT");
+
+ final PlanAlignmentCreate alignment = rules.getPlanCreateAlignment(specifier, catalog);
+ Assert.assertEquals(alignment, PlanAlignmentCreate.START_OF_SUBSCRIPTION);
+
+ final PlanSpecifier specifier2 = new PlanSpecifier("Extra-Ammo", ProductCategory.ADD_ON, BillingPeriod.MONTHLY,
+ "DEFAULT");
+
+ final PlanAlignmentCreate alignment2 = rules.getPlanCreateAlignment(specifier2, catalog);
+ Assert.assertEquals(alignment2, PlanAlignmentCreate.START_OF_BUNDLE);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/rules/TestPlanRules.java b/catalog/src/test/java/org/killbill/billing/catalog/rules/TestPlanRules.java
new file mode 100644
index 0000000..3abe891
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/rules/TestPlanRules.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.rules;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.CatalogTestSuiteNoDB;
+import org.killbill.billing.catalog.DefaultPriceList;
+import org.killbill.billing.catalog.DefaultProduct;
+import org.killbill.billing.catalog.MockCatalog;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.IllegalPlanChange;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.PlanAlignmentChange;
+import org.killbill.billing.catalog.api.PlanChangeResult;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PlanSpecifier;
+import org.killbill.billing.catalog.api.PriceListSet;
+
+public class TestPlanRules extends CatalogTestSuiteNoDB {
+
+ private MockCatalog cat = null;
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() {
+ cat = new MockCatalog();
+
+ final DefaultPriceList priceList2 = cat.getPriceLists().getChildPriceLists()[0];
+
+ final CaseChangePlanPolicy casePolicy = new CaseChangePlanPolicy().setPolicy(BillingActionPolicy.END_OF_TERM);
+ final CaseChangePlanAlignment caseAlignment = new CaseChangePlanAlignment().setAlignment(PlanAlignmentChange.START_OF_SUBSCRIPTION);
+ final CasePriceList casePriceList = new CasePriceList().setToPriceList(priceList2);
+
+ cat.getPlanRules().
+ setChangeCase(new CaseChangePlanPolicy[]{casePolicy}).
+ setChangeAlignmentCase(new CaseChangePlanAlignment[]{caseAlignment}).
+ setPriceListCase(new CasePriceList[]{casePriceList});
+ }
+
+ @Test(groups = "fast")
+ public void testCannotChangeToSamePlan() throws CatalogApiException {
+ final DefaultProduct product1 = cat.getCurrentProducts()[0];
+ final DefaultPriceList priceList1 = cat.findCurrentPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ final PlanPhaseSpecifier from = new PlanPhaseSpecifier(product1.getName(), product1.getCategory(), BillingPeriod.MONTHLY, priceList1.getName(), PhaseType.EVERGREEN);
+ final PlanSpecifier to = new PlanSpecifier(product1.getName(), product1.getCategory(), BillingPeriod.MONTHLY, priceList1.getName());
+
+ try {
+ cat.getPlanRules().planChange(from, to, cat);
+ Assert.fail("We did not see an exception when trying to change plan to the same plan");
+ } catch (IllegalPlanChange e) {
+ // Correct - cannot change to the same plan
+ } catch (CatalogApiException e) {
+ Assert.fail("", e);
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testExistingPriceListIsKept() throws CatalogApiException {
+ final DefaultProduct product1 = cat.getCurrentProducts()[0];
+ final DefaultPriceList priceList1 = cat.findCurrentPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ final PlanPhaseSpecifier from = new PlanPhaseSpecifier(product1.getName(), product1.getCategory(), BillingPeriod.MONTHLY, priceList1.getName(), PhaseType.EVERGREEN);
+ final PlanSpecifier to = new PlanSpecifier(product1.getName(), product1.getCategory(), BillingPeriod.ANNUAL, priceList1.getName());
+
+ PlanChangeResult result = null;
+ try {
+ result = cat.getPlanRules().planChange(from, to, cat);
+ } catch (IllegalPlanChange e) {
+ Assert.fail("We should not have triggered this error");
+ } catch (CatalogApiException e) {
+ Assert.fail("", e);
+ }
+
+ Assert.assertEquals(result.getPolicy(), BillingActionPolicy.END_OF_TERM);
+ Assert.assertEquals(result.getAlignment(), PlanAlignmentChange.START_OF_SUBSCRIPTION);
+ Assert.assertEquals(result.getNewPriceList(), priceList1);
+ }
+
+ @Test(groups = "fast")
+ public void testBaseCase() throws CatalogApiException {
+ final DefaultProduct product1 = cat.getCurrentProducts()[0];
+ final DefaultProduct product2 = cat.getCurrentProducts()[1];
+ final DefaultPriceList priceList1 = cat.findCurrentPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+ final DefaultPriceList priceList2 = cat.getPriceLists().getChildPriceLists()[0];
+
+ final PlanPhaseSpecifier from = new PlanPhaseSpecifier(product1.getName(), product1.getCategory(), BillingPeriod.MONTHLY, priceList1.getName(), PhaseType.EVERGREEN);
+ final PlanSpecifier to = new PlanSpecifier(product2.getName(), product2.getCategory(), BillingPeriod.MONTHLY, null);
+
+ PlanChangeResult result = null;
+ try {
+ result = cat.getPlanRules().planChange(from, to, cat);
+ } catch (IllegalPlanChange e) {
+ Assert.fail("We should not have triggered this error");
+ } catch (CatalogApiException e) {
+ Assert.fail("", e);
+ }
+
+ Assert.assertEquals(result.getPolicy(), BillingActionPolicy.END_OF_TERM);
+ Assert.assertEquals(result.getAlignment(), PlanAlignmentChange.START_OF_SUBSCRIPTION);
+ Assert.assertEquals(result.getNewPriceList(), priceList2);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogService.java b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogService.java
new file mode 100644
index 0000000..9c34586
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogService.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.io.VersionedCatalogLoader;
+import org.killbill.billing.lifecycle.KillbillService.ServiceException;
+import org.killbill.clock.DefaultClock;
+import org.killbill.billing.util.config.CatalogConfig;
+
+public class TestCatalogService extends CatalogTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testCatalogServiceDirectory() throws ServiceException {
+ final DefaultCatalogService service = new DefaultCatalogService(new CatalogConfig() {
+ @Override
+ public String getCatalogURI() {
+ return "file:src/test/resources/versionedCatalog";
+ }
+
+ }, new VersionedCatalogLoader(new DefaultClock()));
+ service.loadCatalog();
+ Assert.assertNotNull(service.getFullCatalog());
+ Assert.assertEquals(service.getFullCatalog().getCatalogName(), "WeaponsHireSmall");
+ }
+
+ @Test(groups = "fast")
+ public void testCatalogServiceFile() throws ServiceException {
+ final DefaultCatalogService service = new DefaultCatalogService(new CatalogConfig() {
+ @Override
+ public String getCatalogURI() {
+ return "file:src/test/resources/WeaponsHire.xml";
+ }
+
+ }, new VersionedCatalogLoader(new DefaultClock()));
+ service.loadCatalog();
+ Assert.assertNotNull(service.getFullCatalog());
+ Assert.assertEquals(service.getFullCatalog().getCatalogName(), "Firearms");
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestInternationalPrice.java b/catalog/src/test/java/org/killbill/billing/catalog/TestInternationalPrice.java
new file mode 100644
index 0000000..dd62253
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestInternationalPrice.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import java.math.BigDecimal;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+public class TestInternationalPrice extends CatalogTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testZeroValue() throws URISyntaxException, CatalogApiException {
+ final StandaloneCatalog c = new MockCatalog();
+ c.setSupportedCurrencies(new Currency[]{Currency.GBP, Currency.EUR, Currency.USD, Currency.BRL, Currency.MXN});
+ final DefaultInternationalPrice p0 = new MockInternationalPrice();
+ p0.setPrices(null);
+ p0.initialize(c, new URI("foo:bar"));
+ final DefaultInternationalPrice p1 = new MockInternationalPrice();
+ p1.setPrices(new DefaultPrice[]{
+ new DefaultPrice().setCurrency(Currency.GBP).setValue(new BigDecimal(1)),
+ new DefaultPrice().setCurrency(Currency.EUR).setValue(new BigDecimal(1)),
+ new DefaultPrice().setCurrency(Currency.USD).setValue(new BigDecimal(1)),
+ new DefaultPrice().setCurrency(Currency.BRL).setValue(new BigDecimal(1)),
+ new DefaultPrice().setCurrency(Currency.MXN).setValue(new BigDecimal(1)),
+ });
+ p1.initialize(c, new URI("foo:bar"));
+
+ Assert.assertEquals(p0.getPrice(Currency.GBP), new BigDecimal(0));
+ Assert.assertEquals(p0.getPrice(Currency.EUR), new BigDecimal(0));
+ Assert.assertEquals(p0.getPrice(Currency.USD), new BigDecimal(0));
+ Assert.assertEquals(p0.getPrice(Currency.BRL), new BigDecimal(0));
+ Assert.assertEquals(p0.getPrice(Currency.MXN), new BigDecimal(0));
+
+ Assert.assertEquals(p1.getPrice(Currency.GBP), new BigDecimal(1));
+ Assert.assertEquals(p1.getPrice(Currency.EUR), new BigDecimal(1));
+ Assert.assertEquals(p1.getPrice(Currency.USD), new BigDecimal(1));
+ Assert.assertEquals(p1.getPrice(Currency.BRL), new BigDecimal(1));
+ Assert.assertEquals(p1.getPrice(Currency.MXN), new BigDecimal(1));
+ }
+
+ @Test(groups = "fast")
+ public void testPriceInitialization() throws URISyntaxException, CatalogApiException {
+ final StandaloneCatalog c = new MockCatalog();
+ c.setSupportedCurrencies(new Currency[]{Currency.GBP, Currency.EUR, Currency.USD, Currency.BRL, Currency.MXN});
+ c.getCurrentPlans()[0].getFinalPhase().getRecurringPrice().setPrices(null);
+ c.initialize(c, new URI("foo://bar"));
+ Assert.assertEquals(c.getCurrentPlans()[0].getFinalPhase().getRecurringPrice().getPrice(Currency.GBP), new BigDecimal(0));
+ }
+
+ @Test(groups = "fast")
+ public void testNegativeValuePrices() {
+ final StandaloneCatalog c = new MockCatalog();
+ c.setSupportedCurrencies(new Currency[]{Currency.GBP, Currency.EUR, Currency.USD, Currency.BRL, Currency.MXN});
+
+ final DefaultInternationalPrice p1 = new MockInternationalPrice();
+ p1.setPrices(new DefaultPrice[]{
+ new DefaultPrice().setCurrency(Currency.GBP).setValue(new BigDecimal(-1)),
+ new DefaultPrice().setCurrency(Currency.EUR).setValue(new BigDecimal(-1)),
+ new DefaultPrice().setCurrency(Currency.USD).setValue(new BigDecimal(-1)),
+ new DefaultPrice().setCurrency(Currency.BRL).setValue(new BigDecimal(1)),
+ new DefaultPrice().setCurrency(Currency.MXN).setValue(new BigDecimal(1)),
+ });
+
+ final ValidationErrors errors = p1.validate(c, new ValidationErrors());
+ errors.log(log);
+ Assert.assertEquals(errors.size(), 3);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestLimits.java b/catalog/src/test/java/org/killbill/billing/catalog/TestLimits.java
new file mode 100644
index 0000000..5e7246b
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestLimits.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.google.common.io.Resources;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.lifecycle.KillbillService.ServiceException;
+
+public class TestLimits extends CatalogTestSuiteNoDB {
+ private VersionedCatalog catalog;
+
+ @BeforeClass(groups = "fast")
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ catalog = loader.load(Resources.getResource("WeaponsHireSmall.xml").toString());
+ }
+
+ @Test(groups = "fast")
+ public void testLimits() throws Exception {
+ PlanPhase phase = catalog.findCurrentPhase("pistol-monthly-evergreen");
+ Assert.assertNotNull(phase);
+
+ //<limits>
+ // <limit>
+ // <unit>targets</unit>
+ // <min>3</min>
+ // </limit>
+ // <limit>
+ // <unit>misfires</unit>
+ // <max>20</max>
+ // </limit>
+ //</limits>
+ Assert.assertTrue(catalog.compliesWithLimits("pistol-monthly-evergreen", "targets", 3));
+ Assert.assertTrue(catalog.compliesWithLimits("pistol-monthly-evergreen", "targets", 2000));
+ Assert.assertFalse(catalog.compliesWithLimits("pistol-monthly-evergreen", "targets", 2));
+
+ Assert.assertTrue(catalog.compliesWithLimits("pistol-monthly-evergreen", "misfires", 3));
+ Assert.assertFalse(catalog.compliesWithLimits("pistol-monthly-evergreen", "misfires", 21));
+ Assert.assertTrue(catalog.compliesWithLimits("pistol-monthly-evergreen", "misfires", -1));
+/* <product name="Shotgun">
+ <category>BASE</category>
+ <limits>
+ <limit>
+ <unit>shells</unit>
+ <max>300</max>
+ </limit>
+ </limits>
+ </product>
+ <plan name="shotgun-annual">
+ <product>Shotgun</product>
+ ...
+ <finalPhase type="EVERGREEN">
+ <limits>
+ <limit>
+ <unit>shells</unit>
+ <max>200</max>
+ </limit>
+ </limits>
+ </finalPhase>
+ </plan>
+*/
+ Assert.assertTrue(catalog.compliesWithLimits("shotgun-monthly-evergreen", "shells", 100));
+ Assert.assertFalse(catalog.compliesWithLimits("shotgun-monthly-evergreen", "shells", 400));
+ Assert.assertTrue(catalog.compliesWithLimits("shotgun-monthly-evergreen", "shells", 250));
+
+ Assert.assertTrue(catalog.compliesWithLimits("shotgun-annual-evergreen", "shells", 100));
+ Assert.assertFalse(catalog.compliesWithLimits("shotgun-annual-evergreen", "shells", 400));
+ Assert.assertFalse(catalog.compliesWithLimits("shotgun-annual-evergreen", "shells", 250));
+
+
+
+
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestPlan.java b/catalog/src/test/java/org/killbill/billing/catalog/TestPlan.java
new file mode 100644
index 0000000..67c2404
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestPlan.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import java.util.Date;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+public class TestPlan extends CatalogTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testDateValidation() {
+ final StandaloneCatalog c = new MockCatalog();
+ c.setSupportedCurrencies(new Currency[]{Currency.GBP, Currency.EUR, Currency.USD, Currency.BRL, Currency.MXN});
+ final DefaultPlan p1 = MockPlan.createBicycleTrialEvergreen1USD();
+ p1.setEffectiveDateForExistingSubscriptons(new Date((new Date().getTime()) - (1000 * 60 * 60 * 24)));
+ final ValidationErrors errors = p1.validate(c, new ValidationErrors());
+ Assert.assertEquals(errors.size(), 1);
+ errors.log(log);
+ }
+
+ @Test(groups = "fast")
+ public void testDataCalc() {
+ final DefaultPlan p0 = MockPlan.createBicycleTrialEvergreen1USD();
+
+ final DefaultPlan p1 = MockPlan.createBicycleTrialEvergreen1USD(100);
+
+ final DefaultPlan p2 = MockPlan.createBicycleNoTrialEvergreen1USD();
+
+ final DateTime requestedDate = new DateTime();
+ Assert.assertEquals(p0.dateOfFirstRecurringNonZeroCharge(requestedDate, null).compareTo(requestedDate.plusDays(30)), 0);
+ Assert.assertEquals(p1.dateOfFirstRecurringNonZeroCharge(requestedDate, null).compareTo(requestedDate.plusDays(100)), 0);
+ Assert.assertEquals(p2.dateOfFirstRecurringNonZeroCharge(requestedDate, null).compareTo(requestedDate.plusDays(0)), 0);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestPlanPhase.java b/catalog/src/test/java/org/killbill/billing/catalog/TestPlanPhase.java
new file mode 100644
index 0000000..7fcf043
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestPlanPhase.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+public class TestPlanPhase extends CatalogTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testValidation() {
+ DefaultPlanPhase pp = MockPlanPhase.createUSDMonthlyEvergreen(null, "1.00").setPlan(MockPlan.createBicycleNoTrialEvergreen1USD());//new MockPlanPhase().setBillCycleDuration(BillingPeriod.MONTHLY).setRecurringPrice(null).setFixedPrice(new DefaultInternationalPrice());
+
+ ValidationErrors errors = pp.validate(new MockCatalog(), new ValidationErrors());
+ errors.log(log);
+ Assert.assertEquals(errors.size(), 1);
+
+ pp = MockPlanPhase.createUSDMonthlyEvergreen("1.00", null).setBillCycleDuration(BillingPeriod.NO_BILLING_PERIOD).setPlan(MockPlan.createBicycleNoTrialEvergreen1USD());// new MockPlanPhase().setBillCycleDuration(BillingPeriod.NO_BILLING_PERIOD).setRecurringPrice(new MockInternationalPrice());
+ errors = pp.validate(new MockCatalog(), new ValidationErrors());
+ errors.log(log);
+ Assert.assertEquals(errors.size(), 2);
+
+ pp = MockPlanPhase.createUSDMonthlyEvergreen(null, null).setBillCycleDuration(BillingPeriod.NO_BILLING_PERIOD).setPlan(MockPlan.createBicycleNoTrialEvergreen1USD());//new MockPlanPhase().setRecurringPrice(null).setFixedPrice(null).setBillCycleDuration(BillingPeriod.NO_BILLING_PERIOD);
+ errors = pp.validate(new MockCatalog(), new ValidationErrors());
+ errors.log(log);
+ Assert.assertEquals(errors.size(), 2);
+ }
+
+ @Test(groups = "fast")
+ public void testPhaseNames() throws CatalogApiException {
+ final String planName = "Foo";
+ final String planNameExt = planName + "-";
+
+ final DefaultPlan p = MockPlan.createBicycleNoTrialEvergreen1USD().setName(planName);
+ final DefaultPlanPhase ppDiscount = MockPlanPhase.create1USDMonthlyEvergreen().setPhaseType(PhaseType.DISCOUNT).setPlan(p);
+ final DefaultPlanPhase ppTrial = MockPlanPhase.create30DayTrial().setPhaseType(PhaseType.TRIAL).setPlan(p);
+ final DefaultPlanPhase ppEvergreen = MockPlanPhase.create1USDMonthlyEvergreen().setPhaseType(PhaseType.EVERGREEN).setPlan(p);
+ final DefaultPlanPhase ppFixedTerm = MockPlanPhase.create1USDMonthlyEvergreen().setPhaseType(PhaseType.FIXEDTERM).setPlan(p);
+
+ final String ppnDiscount = DefaultPlanPhase.phaseName(p.getName(), ppDiscount.getPhaseType());
+ final String ppnTrial = DefaultPlanPhase.phaseName(p.getName(), ppTrial.getPhaseType());
+ final String ppnEvergreen = DefaultPlanPhase.phaseName(p.getName(), ppEvergreen.getPhaseType());
+ final String ppnFixedTerm = DefaultPlanPhase.phaseName(p.getName(), ppFixedTerm.getPhaseType());
+
+ Assert.assertEquals(ppnTrial, planNameExt + "trial");
+ Assert.assertEquals(ppnEvergreen, planNameExt + "evergreen");
+ Assert.assertEquals(ppnFixedTerm, planNameExt + "fixedterm");
+ Assert.assertEquals(ppnDiscount, planNameExt + "discount");
+
+ Assert.assertEquals(DefaultPlanPhase.planName(ppnDiscount), planName);
+ Assert.assertEquals(DefaultPlanPhase.planName(ppnTrial), planName);
+ Assert.assertEquals(DefaultPlanPhase.planName(ppnEvergreen), planName);
+ Assert.assertEquals(DefaultPlanPhase.planName(ppnFixedTerm), planName);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestPriceListSet.java b/catalog/src/test/java/org/killbill/billing/catalog/TestPriceListSet.java
new file mode 100644
index 0000000..c215fd7
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestPriceListSet.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+
+import static org.killbill.billing.catalog.api.BillingPeriod.ANNUAL;
+import static org.killbill.billing.catalog.api.BillingPeriod.MONTHLY;
+import static org.killbill.billing.catalog.api.PhaseType.DISCOUNT;
+import static org.killbill.billing.catalog.api.PhaseType.EVERGREEN;
+
+public class TestPriceListSet extends CatalogTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testOverriding() throws CatalogApiException {
+ final DefaultProduct foo = new DefaultProduct("Foo", ProductCategory.BASE);
+ final DefaultProduct bar = new DefaultProduct("Bar", ProductCategory.BASE);
+ final DefaultPlan[] defaultPlans = new DefaultPlan[]{
+ new MockPlan().setName("plan-foo-monthly").setProduct(foo).setFinalPhase(new MockPlanPhase().setBillCycleDuration(MONTHLY).setPhaseType(EVERGREEN)),
+ new MockPlan().setName("plan-bar-monthly").setProduct(bar).setFinalPhase(new MockPlanPhase().setBillCycleDuration(MONTHLY).setPhaseType(EVERGREEN)),
+ new MockPlan().setName("plan-foo-annual").setProduct(foo).setFinalPhase(new MockPlanPhase().setBillCycleDuration(ANNUAL).setPhaseType(EVERGREEN)),
+ new MockPlan().setName("plan-bar-annual").setProduct(bar).setFinalPhase(new MockPlanPhase().setBillCycleDuration(ANNUAL).setPhaseType(EVERGREEN))
+ };
+ final DefaultPlan[] childPlans = new DefaultPlan[]{
+ new MockPlan().setName("plan-foo").setProduct(foo).setFinalPhase(new MockPlanPhase().setBillCycleDuration(ANNUAL).setPhaseType(DISCOUNT)),
+ new MockPlan().setName("plan-bar").setProduct(bar).setFinalPhase(new MockPlanPhase().setBillCycleDuration(ANNUAL).setPhaseType(DISCOUNT))
+ };
+ final PriceListDefault defaultPriceList = new PriceListDefault(defaultPlans);
+ final DefaultPriceList[] childPriceLists = new DefaultPriceList[]{
+ new DefaultPriceList(childPlans, "child")
+ };
+ final DefaultPriceListSet set = new DefaultPriceListSet(defaultPriceList, childPriceLists);
+
+ Assert.assertEquals(set.getPlanFrom(PriceListSet.DEFAULT_PRICELIST_NAME, foo, BillingPeriod.ANNUAL).getFinalPhase().getPhaseType(), PhaseType.EVERGREEN);
+ Assert.assertEquals(set.getPlanFrom(PriceListSet.DEFAULT_PRICELIST_NAME, foo, BillingPeriod.MONTHLY).getFinalPhase().getPhaseType(), PhaseType.EVERGREEN);
+ Assert.assertEquals(set.getPlanFrom("child", foo, BillingPeriod.ANNUAL).getFinalPhase().getPhaseType(), PhaseType.DISCOUNT);
+ Assert.assertEquals(set.getPlanFrom("child", foo, BillingPeriod.MONTHLY).getFinalPhase().getPhaseType(), PhaseType.EVERGREEN);
+ }
+
+ @Test(groups = "fast")
+ public void testForNullBillingPeriod() throws CatalogApiException {
+ final DefaultProduct foo = new DefaultProduct("Foo", ProductCategory.BASE);
+ final DefaultProduct bar = new DefaultProduct("Bar", ProductCategory.BASE);
+ final DefaultPlan[] defaultPlans = new DefaultPlan[]{
+ new MockPlan().setName("plan-foo-monthly").setProduct(foo).setFinalPhase(new MockPlanPhase().setBillCycleDuration(MONTHLY).setPhaseType(EVERGREEN)),
+ new MockPlan().setName("plan-bar-monthly").setProduct(bar).setFinalPhase(new MockPlanPhase().setBillCycleDuration(MONTHLY).setPhaseType(EVERGREEN)),
+ new MockPlan().setName("plan-foo-annual").setProduct(foo).setFinalPhase(new MockPlanPhase().setBillCycleDuration(null).setPhaseType(EVERGREEN)),
+ new MockPlan().setName("plan-bar-annual").setProduct(bar).setFinalPhase(new MockPlanPhase().setBillCycleDuration(null).setPhaseType(EVERGREEN))
+ };
+ final DefaultPlan[] childPlans = new DefaultPlan[]{
+ new MockPlan().setName("plan-foo").setProduct(foo).setFinalPhase(new MockPlanPhase().setBillCycleDuration(ANNUAL).setPhaseType(DISCOUNT)),
+ new MockPlan().setName("plan-bar").setProduct(bar).setFinalPhase(new MockPlanPhase().setBillCycleDuration(ANNUAL).setPhaseType(DISCOUNT))
+ };
+
+ final PriceListDefault defaultPriceList = new PriceListDefault(defaultPlans);
+ final DefaultPriceList[] childPriceLists = new DefaultPriceList[]{
+ new DefaultPriceList(childPlans, "child")
+ };
+ final DefaultPriceListSet set = new DefaultPriceListSet(defaultPriceList, childPriceLists);
+
+ Assert.assertEquals(set.getPlanFrom("child", foo, BillingPeriod.ANNUAL).getFinalPhase().getPhaseType(), PhaseType.DISCOUNT);
+ Assert.assertEquals(set.getPlanFrom("child", foo, BillingPeriod.MONTHLY).getFinalPhase().getPhaseType(), PhaseType.EVERGREEN);
+ Assert.assertEquals(set.getPlanFrom(PriceListSet.DEFAULT_PRICELIST_NAME, foo, BillingPeriod.ANNUAL).getFinalPhase().getPhaseType(), PhaseType.EVERGREEN);
+ Assert.assertEquals(set.getPlanFrom(PriceListSet.DEFAULT_PRICELIST_NAME, foo, BillingPeriod.MONTHLY).getFinalPhase().getPhaseType(), PhaseType.EVERGREEN);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java
new file mode 100644
index 0000000..93b491e
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PhaseType;
+
+public class TestStandaloneCatalog extends CatalogTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testFindPhase() throws CatalogApiException {
+ final DefaultPlanPhase phaseTrial1 = new MockPlanPhase().setPhaseType(PhaseType.TRIAL);
+ final DefaultPlanPhase phaseTrial2 = new MockPlanPhase().setPhaseType(PhaseType.TRIAL);
+ final DefaultPlanPhase phaseDiscount1 = new MockPlanPhase().setPhaseType(PhaseType.DISCOUNT);
+ final DefaultPlanPhase phaseDiscount2 = new MockPlanPhase().setPhaseType(PhaseType.DISCOUNT);
+
+ final DefaultPlan plan1 = new MockPlan().setName("TestPlan1").setFinalPhase(phaseDiscount1).setInitialPhases(new DefaultPlanPhase[]{phaseTrial1});
+ final DefaultPlan plan2 = new MockPlan().setName("TestPlan2").setFinalPhase(phaseDiscount2).setInitialPhases(new DefaultPlanPhase[]{phaseTrial2});
+ phaseTrial1.setPlan(plan1);
+ phaseTrial2.setPlan(plan2);
+ phaseDiscount1.setPlan(plan1);
+ phaseDiscount2.setPlan(plan2);
+
+ final StandaloneCatalog cat = new MockCatalog().setPlans(new DefaultPlan[]{plan1, plan2});
+
+ Assert.assertEquals(cat.findCurrentPhase("TestPlan1-discount"), phaseDiscount1);
+ Assert.assertEquals(cat.findCurrentPhase("TestPlan2-discount"), phaseDiscount2);
+ Assert.assertEquals(cat.findCurrentPhase("TestPlan1-trial"), phaseTrial1);
+ Assert.assertEquals(cat.findCurrentPhase("TestPlan2-trial"), phaseTrial2);
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java b/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java
new file mode 100644
index 0000000..32a6354
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestVersionedCatalog.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.URISyntaxException;
+import java.util.Date;
+
+import javax.xml.bind.JAXBException;
+import javax.xml.transform.TransformerException;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+import org.xml.sax.SAXException;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.InvalidConfigException;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.lifecycle.KillbillService.ServiceException;
+
+import com.google.common.io.Resources;
+
+public class TestVersionedCatalog extends CatalogTestSuiteNoDB {
+
+ private VersionedCatalog vc;
+
+ @BeforeClass(groups = "fast")
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ vc = loader.load(Resources.getResource("versionedCatalog").toString());
+ }
+
+ @Test(groups = "fast")
+ public void testAddCatalog() throws IOException, SAXException, InvalidConfigException, JAXBException, TransformerException, URISyntaxException, ServiceException, CatalogApiException {
+ vc.add(new StandaloneCatalog(new Date()));
+ Assert.assertEquals(vc.size(), 4);
+ }
+
+ @Test(groups = "fast")
+ public void testFindPlanWithDates() throws Exception {
+ final DateTime dt0 = new DateTime("2010-01-01T00:00:00+00:00");
+ final DateTime dt1 = new DateTime("2011-01-01T00:01:00+00:00");
+ final DateTime dt2 = new DateTime("2011-02-02T00:01:00+00:00");
+ 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);
+
+ }
+ 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);
+ final Plan newSubPlan3 = vc.findPlan("pistol-monthly", dt3, dt3);
+
+ Assert.assertEquals(newSubPlan1.getAllPhases()[1].getRecurringPrice().getPrice(Currency.USD), new BigDecimal("1.0"));
+ Assert.assertEquals(newSubPlan2.getAllPhases()[1].getRecurringPrice().getPrice(Currency.USD), new BigDecimal("2.0"));
+ Assert.assertEquals(newSubPlan214.getAllPhases()[1].getRecurringPrice().getPrice(Currency.USD), new BigDecimal("2.0"));
+ Assert.assertEquals(newSubPlan3.getAllPhases()[1].getRecurringPrice().getPrice(Currency.USD), new BigDecimal("3.0"));
+
+ // Existing subscription
+
+ final Plan exSubPlan2 = vc.findPlan("pistol-monthly", dt2, dt1);
+ final Plan exSubPlan214 = vc.findPlan("pistol-monthly", dt214, dt1);
+ final Plan exSubPlan3 = vc.findPlan("pistol-monthly", dt3, dt1);
+
+ Assert.assertEquals(exSubPlan2.getAllPhases()[1].getRecurringPrice().getPrice(Currency.USD), new BigDecimal("1.0"));
+ Assert.assertEquals(exSubPlan214.getAllPhases()[1].getRecurringPrice().getPrice(Currency.USD), new BigDecimal("2.0"));
+ Assert.assertEquals(exSubPlan3.getAllPhases()[1].getRecurringPrice().getPrice(Currency.USD), new BigDecimal("2.0"));
+
+ }
+
+ @Test(groups = "fast")
+ public void testErrorOnDateTooEarly() {
+ final DateTime dt0 = new DateTime("1977-01-01T00:00:00+00:00");
+ try {
+ vc.findPlan("foo", dt0);
+ Assert.fail("Date is too early an exception should have been thrown");
+ } catch (CatalogApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.CAT_NO_CATALOG_FOR_GIVEN_DATE.getCode());
+ }
+ }
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/util/CreateCatalogSchema.java b/catalog/src/test/java/org/killbill/billing/catalog/util/CreateCatalogSchema.java
new file mode 100644
index 0000000..fc9eece
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/util/CreateCatalogSchema.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.catalog.util;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.Writer;
+
+import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.util.config.catalog.XMLSchemaGenerator;
+
+// Tool to print the catalog XML Schema (XSD)
+public class CreateCatalogSchema {
+
+ public static void main(final String[] args) throws Exception {
+ if (args.length != 1) {
+ System.err.println("Usage: <filepath>");
+ System.exit(0);
+ }
+
+ final File f = new File(args[0]);
+ final Writer w = new FileWriter(f);
+ w.write(XMLSchemaGenerator.xmlSchemaAsString(StandaloneCatalog.class));
+ w.close();
+ }
+}
currency/pom.xml 52(+16 -36)
diff --git a/currency/pom.xml b/currency/pom.xml
index 311d8ef..a943b91 100644
--- a/currency/pom.xml
+++ b/currency/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -48,87 +48,67 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-currency</artifactId>
</dependency>
<dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!--
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-payment</artifactId>
</dependency>
<dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
diff --git a/currency/src/main/java/org/killbill/billing/currency/api/DefaultCurrencyConversion.java b/currency/src/main/java/org/killbill/billing/currency/api/DefaultCurrencyConversion.java
new file mode 100644
index 0000000..c833901
--- /dev/null
+++ b/currency/src/main/java/org/killbill/billing/currency/api/DefaultCurrencyConversion.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.currency.api;
+
+import java.util.Set;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+
+public class DefaultCurrencyConversion implements CurrencyConversion {
+
+ private final Currency baseCurrency;
+ private final Set<Rate> rates;
+
+ public DefaultCurrencyConversion(final Currency baseCurrency, final Set<Rate> rates) {
+ this.baseCurrency = baseCurrency;
+ this.rates = rates;
+ }
+
+ @Override
+ public Currency getBaseCurrency() {
+ return baseCurrency;
+ }
+
+ @Override
+ public final Set<Rate> getRates() {
+ return rates;
+ }
+}
diff --git a/currency/src/main/java/org/killbill/billing/currency/api/DefaultCurrencyConversionApi.java b/currency/src/main/java/org/killbill/billing/currency/api/DefaultCurrencyConversionApi.java
new file mode 100644
index 0000000..e9e4559
--- /dev/null
+++ b/currency/src/main/java/org/killbill/billing/currency/api/DefaultCurrencyConversionApi.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.currency.api;
+
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.currency.plugin.api.CurrencyPluginApi;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.util.config.CurrencyConfig;
+
+public class DefaultCurrencyConversionApi implements CurrencyConversionApi {
+
+ private final CurrencyConfig config;
+ private final OSGIServiceRegistration<CurrencyPluginApi> registry;
+
+ @Inject
+ public DefaultCurrencyConversionApi(final CurrencyConfig config, final OSGIServiceRegistration<CurrencyPluginApi> registry) {
+ this.config = config;
+ this.registry = registry;
+
+ }
+
+ private CurrencyPluginApi getPluginApi() throws CurrencyConversionException {
+ final CurrencyPluginApi result = registry.getServiceForName(config.getDefaultCurrencyProvider());
+ if (result == null) {
+ throw new CurrencyConversionException(ErrorCode.CURRENCY_NO_SUCH_PAYMENT_PLUGIN, config.getDefaultCurrencyProvider());
+ }
+ return result;
+ }
+
+ @Override
+ public Set<Currency> getBaseRates() throws CurrencyConversionException {
+ final CurrencyPluginApi pluginApi = getPluginApi();
+ return pluginApi.getBaseCurrencies();
+ }
+
+ @Override
+ public CurrencyConversion getCurrentCurrencyConversion(final Currency baseCurrency) throws CurrencyConversionException {
+ final Set<Rate> allRates = getPluginApi().getCurrentRates(baseCurrency);
+ return getCurrencyConversionInternal(baseCurrency, allRates);
+ }
+
+ @Override
+ public CurrencyConversion getCurrencyConversion(final Currency baseCurrency, final DateTime dateConversion) throws CurrencyConversionException {
+ final Set<Rate> allRates = getPluginApi().getRates(baseCurrency, dateConversion);
+ return getCurrencyConversionInternal(baseCurrency, allRates);
+ }
+
+ private CurrencyConversion getCurrencyConversionInternal(final Currency baseCurrency, final Set<Rate> allRates) {
+ final CurrencyConversion result = new DefaultCurrencyConversion(baseCurrency, allRates);
+ return result;
+ }
+}
diff --git a/currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyProviderPluginRegistry.java b/currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyProviderPluginRegistry.java
new file mode 100644
index 0000000..abb6abd
--- /dev/null
+++ b/currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyProviderPluginRegistry.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.currency;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.currency.plugin.api.CurrencyPluginApi;
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.util.config.CurrencyConfig;
+
+import com.google.inject.Inject;
+
+public class DefaultCurrencyProviderPluginRegistry implements OSGIServiceRegistration<CurrencyPluginApi> {
+
+ private final static Logger log = LoggerFactory.getLogger(DefaultCurrencyProviderPluginRegistry.class);
+
+ private final Map<String, CurrencyPluginApi> pluginsByName = new ConcurrentHashMap<String, CurrencyPluginApi>();
+
+ @Inject
+ public DefaultCurrencyProviderPluginRegistry() {
+ }
+
+ @Override
+ public void registerService(final OSGIServiceDescriptor desc, final CurrencyPluginApi service) {
+ log.info("DefaultCurrencyProviderPluginRegistry registering service " + desc.getRegistrationName());
+ pluginsByName.put(desc.getRegistrationName(), service);
+ }
+
+ @Override
+ public void unregisterService(final String serviceName) {
+ log.info("DefaultCurrencyProviderPluginRegistry unregistering service " + serviceName);
+ pluginsByName.remove(serviceName);
+ }
+
+ @Override
+ public CurrencyPluginApi getServiceForName(final String serviceName) {
+ return pluginsByName.get(serviceName);
+ }
+
+ @Override
+ public Set<String> getAllServices() {
+ return pluginsByName.keySet();
+ }
+
+ @Override
+ public Class<CurrencyPluginApi> getServiceType() {
+ return CurrencyPluginApi.class;
+ }
+}
diff --git a/currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyService.java b/currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyService.java
new file mode 100644
index 0000000..0b3e9b8
--- /dev/null
+++ b/currency/src/main/java/org/killbill/billing/currency/DefaultCurrencyService.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.currency;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.currency.api.CurrencyService;
+
+public class DefaultCurrencyService implements CurrencyService {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultCurrencyService.class);
+
+ public static final String SERVICE_NAME = "currency-service";
+
+ @Override
+ public String getName() {
+ return SERVICE_NAME;
+ }
+}
diff --git a/currency/src/main/java/org/killbill/billing/currency/glue/CurrencyModule.java b/currency/src/main/java/org/killbill/billing/currency/glue/CurrencyModule.java
new file mode 100644
index 0000000..48287c8
--- /dev/null
+++ b/currency/src/main/java/org/killbill/billing/currency/glue/CurrencyModule.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.currency.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.currency.DefaultCurrencyService;
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.currency.api.CurrencyService;
+import org.killbill.billing.currency.api.DefaultCurrencyConversionApi;
+import org.killbill.billing.currency.plugin.api.CurrencyPluginApi;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.util.config.CurrencyConfig;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.TypeLiteral;
+
+public class CurrencyModule extends AbstractModule {
+
+
+ protected ConfigSource configSource;
+
+ public CurrencyModule(ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ @Override
+ protected void configure() {
+
+ final ConfigurationObjectFactory factory = new ConfigurationObjectFactory(configSource);
+ final CurrencyConfig currencyConfig = factory.build(CurrencyConfig.class);
+ bind(CurrencyConfig.class).toInstance(currencyConfig);
+
+ bind(new TypeLiteral<OSGIServiceRegistration<CurrencyPluginApi>>() {}).toProvider(DefaultCurrencyProviderPluginRegistryProvider.class).asEagerSingleton();
+
+ bind(CurrencyConversionApi.class).to(DefaultCurrencyConversionApi.class).asEagerSingleton();
+ bind(CurrencyService.class).to(DefaultCurrencyService.class).asEagerSingleton();
+ }
+}
diff --git a/currency/src/main/java/org/killbill/billing/currency/glue/DefaultCurrencyProviderPluginRegistryProvider.java b/currency/src/main/java/org/killbill/billing/currency/glue/DefaultCurrencyProviderPluginRegistryProvider.java
new file mode 100644
index 0000000..6e609b3
--- /dev/null
+++ b/currency/src/main/java/org/killbill/billing/currency/glue/DefaultCurrencyProviderPluginRegistryProvider.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.currency.glue;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.currency.DefaultCurrencyProviderPluginRegistry;
+import org.killbill.billing.currency.plugin.api.CurrencyPluginApi;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+
+import com.google.inject.Provider;
+
+public class DefaultCurrencyProviderPluginRegistryProvider implements Provider<OSGIServiceRegistration<CurrencyPluginApi>> {
+
+
+ @Inject
+ public DefaultCurrencyProviderPluginRegistryProvider() {
+ }
+
+ @Override
+ public OSGIServiceRegistration<CurrencyPluginApi> get() {
+ final DefaultCurrencyProviderPluginRegistry pluginRegistry = new DefaultCurrencyProviderPluginRegistry();
+ return pluginRegistry;
+ }
+}
entitlement/pom.xml 84(+32 -52)
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 62cb5e7..576930b 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-entitlement</artifactId>
@@ -45,107 +45,87 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.antlr</groupId>
+ <artifactId>stringtemplate</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-account</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-subscription</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>javax.inject</groupId>
- <artifactId>javax.inject</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.antlr</groupId>
- <artifactId>stringtemplate</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultBlockingTransitionInternalEvent.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultBlockingTransitionInternalEvent.java
new file mode 100644
index 0000000..f32a19c
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultBlockingTransitionInternalEvent.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api;
+
+import java.util.UUID;
+
+import org.killbill.billing.events.BlockingTransitionInternalEvent;
+import org.killbill.billing.events.BusEventBase;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultBlockingTransitionInternalEvent extends BusEventBase implements BlockingTransitionInternalEvent {
+
+ private final UUID blockableId;
+ private final BlockingStateType blockingType;
+ private final Boolean isTransitionedToBlockedBilling;
+ private final Boolean isTransitionedToUnblockedBilling;
+ private final Boolean isTransitionedToBlockedEntitlement;
+ private final Boolean isTransitionedToUnblockedEntitlement;
+
+ @JsonCreator
+ public DefaultBlockingTransitionInternalEvent(@JsonProperty("blockableId") final UUID blockableId,
+ @JsonProperty("blockingType") final BlockingStateType blockingType,
+ @JsonProperty("isTransitionedToBlockedBilling") final Boolean transitionedToBlockedBilling,
+ @JsonProperty("isTransitionedToUnblockedBilling") final Boolean transitionedToUnblockedBilling,
+ @JsonProperty("isTransitionedToBlockedEntitlement") final Boolean transitionedToBlockedEntitlement,
+ @JsonProperty("isTransitionedToUnblockedEntitlement") final Boolean transitionedToUnblockedEntitlement,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.blockableId = blockableId;
+ this.blockingType = blockingType;
+ isTransitionedToBlockedBilling = transitionedToBlockedBilling;
+ isTransitionedToUnblockedBilling = transitionedToUnblockedBilling;
+ isTransitionedToBlockedEntitlement = transitionedToBlockedEntitlement;
+ isTransitionedToUnblockedEntitlement = transitionedToUnblockedEntitlement;
+ }
+
+ @Override
+ public UUID getBlockableId() {
+ return blockableId;
+ }
+
+ @Override
+ public BlockingStateType getBlockingType() {
+ return blockingType;
+ }
+
+ @JsonProperty("isTransitionedToBlockedBilling")
+ @Override
+ public Boolean isTransitionedToBlockedBilling() {
+ return isTransitionedToBlockedBilling;
+ }
+
+ @JsonProperty("isTransitionedToUnblockedBilling")
+ @Override
+ public Boolean isTransitionedToUnblockedBilling() {
+ return isTransitionedToUnblockedBilling;
+ }
+
+ @JsonProperty("isTransitionedToBlockedEntitlement")
+ @Override
+ public Boolean isTransitionedToBlockedEntitlement() {
+ return isTransitionedToBlockedEntitlement;
+ }
+
+ @JsonProperty("isTransitionedToUnblockedEntitlement")
+ @Override
+ public Boolean isTransitionedToUnblockedEntitlement() {
+ return isTransitionedToUnblockedEntitlement;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.BLOCKING_STATE;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DefaultBlockingTransitionInternalEvent)) {
+ return false;
+ }
+
+ final DefaultBlockingTransitionInternalEvent that = (DefaultBlockingTransitionInternalEvent) o;
+
+ if (blockableId != null ? !blockableId.equals(that.blockableId) : that.blockableId != null) {
+ return false;
+ }
+ if (blockingType != that.blockingType) {
+ return false;
+ }
+ if (isTransitionedToBlockedBilling != null ? !isTransitionedToBlockedBilling.equals(that.isTransitionedToBlockedBilling) : that.isTransitionedToBlockedBilling != null) {
+ return false;
+ }
+ if (isTransitionedToBlockedEntitlement != null ? !isTransitionedToBlockedEntitlement.equals(that.isTransitionedToBlockedEntitlement) : that.isTransitionedToBlockedEntitlement != null) {
+ return false;
+ }
+ if (isTransitionedToUnblockedBilling != null ? !isTransitionedToUnblockedBilling.equals(that.isTransitionedToUnblockedBilling) : that.isTransitionedToUnblockedBilling != null) {
+ return false;
+ }
+ if (isTransitionedToUnblockedEntitlement != null ? !isTransitionedToUnblockedEntitlement.equals(that.isTransitionedToUnblockedEntitlement) : that.isTransitionedToUnblockedEntitlement != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = blockableId != null ? blockableId.hashCode() : 0;
+ result = 31 * result + (blockingType != null ? blockingType.hashCode() : 0);
+ result = 31 * result + (isTransitionedToBlockedBilling != null ? isTransitionedToBlockedBilling.hashCode() : 0);
+ result = 31 * result + (isTransitionedToUnblockedBilling != null ? isTransitionedToUnblockedBilling.hashCode() : 0);
+ result = 31 * result + (isTransitionedToBlockedEntitlement != null ? isTransitionedToBlockedEntitlement.hashCode() : 0);
+ result = 31 * result + (isTransitionedToUnblockedEntitlement != null ? isTransitionedToUnblockedEntitlement.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultBlockingTransitionInternalEvent{");
+ sb.append("blockableId=").append(blockableId);
+ sb.append(", blockingType=").append(blockingType);
+ sb.append(", isTransitionedToBlockedBilling=").append(isTransitionedToBlockedBilling);
+ sb.append(", isTransitionedToUnblockedBilling=").append(isTransitionedToUnblockedBilling);
+ sb.append(", isTransitionedToBlockedEntitlement=").append(isTransitionedToBlockedEntitlement);
+ sb.append(", isTransitionedToUnblockedEntitlement=").append(isTransitionedToUnblockedEntitlement);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEffectiveEntitlementEvent.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEffectiveEntitlementEvent.java
new file mode 100644
index 0000000..28b631d
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEffectiveEntitlementEvent.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.entitlement.EntitlementTransitionType;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.EffectiveEntitlementInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultEffectiveEntitlementEvent extends BusEventBase implements EffectiveEntitlementInternalEvent {
+
+ private final UUID id;
+ private final UUID entitlementId;
+ private final UUID bundleId;
+ private final UUID accountId;
+ private final EntitlementTransitionType transitionType;
+ private final DateTime effectiveTransitionTime;
+ private final DateTime requestedTransitionTime;
+
+ @JsonCreator
+ public DefaultEffectiveEntitlementEvent(@JsonProperty("eventId") final UUID id,
+ @JsonProperty("entitlementId") final UUID entitlementId,
+ @JsonProperty("bundleId") final UUID bundleId,
+ @JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("transitionType") final EntitlementTransitionType transitionType,
+ @JsonProperty("effectiveTransitionTime") final DateTime effectiveTransitionTime,
+ @JsonProperty("requestedTransitionTime") final DateTime requestedTransitionTime,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+
+ super(searchKey1, searchKey2, userToken);
+ this.id = id;
+ this.entitlementId = entitlementId;
+ this.bundleId = bundleId;
+ this.accountId = accountId;
+ this.transitionType = transitionType;
+ this.effectiveTransitionTime = effectiveTransitionTime;
+ this.requestedTransitionTime = requestedTransitionTime;
+ }
+
+ @JsonProperty("eventId")
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public UUID getEntitlementId() {
+ return entitlementId;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public EntitlementTransitionType getTransitionType() {
+ return transitionType;
+ }
+
+ @Override
+ public DateTime getRequestedTransitionTime() {
+ return requestedTransitionTime;
+ }
+
+ @Override
+ public DateTime getEffectiveTransitionTime() {
+ return effectiveTransitionTime;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.ENTITLEMENT_TRANSITION;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultEffectiveEntitlementEvent{");
+ sb.append("id=").append(id);
+ sb.append(", entitlementId=").append(entitlementId);
+ sb.append(", bundleId=").append(bundleId);
+ sb.append(", accountId=").append(accountId);
+ sb.append(", transitionType=").append(transitionType);
+ sb.append(", effectiveTransitionTime=").append(effectiveTransitionTime);
+ sb.append(", requestedTransitionTime=").append(requestedTransitionTime);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultEffectiveEntitlementEvent that = (DefaultEffectiveEntitlementEvent) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (effectiveTransitionTime != null ? effectiveTransitionTime.compareTo(that.effectiveTransitionTime) != 0 : that.effectiveTransitionTime != null) {
+ return false;
+ }
+ if (entitlementId != null ? !entitlementId.equals(that.entitlementId) : that.entitlementId != null) {
+ return false;
+ }
+ if (id != null ? !id.equals(that.id) : that.id != null) {
+ return false;
+ }
+ if (requestedTransitionTime != null ? requestedTransitionTime.compareTo(that.requestedTransitionTime) != 0 : that.requestedTransitionTime != null) {
+ return false;
+ }
+ if (transitionType != that.transitionType) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + (entitlementId != null ? entitlementId.hashCode() : 0);
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (transitionType != null ? transitionType.hashCode() : 0);
+ result = 31 * result + (effectiveTransitionTime != null ? effectiveTransitionTime.hashCode() : 0);
+ result = 31 * result + (requestedTransitionTime != null ? requestedTransitionTime.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java
new file mode 100644
index 0000000..e422e72
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api;
+
+import java.util.Collection;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+public class DefaultSubscription extends DefaultEntitlement implements Subscription {
+
+ private final Collection<BlockingState> currentSubscriptionBlockingStatesForServices;
+
+ DefaultSubscription(final DefaultEntitlement entitlement) {
+ super(entitlement);
+ this.currentSubscriptionBlockingStatesForServices = eventsStream.getCurrentSubscriptionEntitlementBlockingStatesForServices();
+ }
+
+ @Override
+ public LocalDate getBillingStartDate() {
+ return new LocalDate(getSubscriptionBase().getStartDate(), getAccountTimeZone());
+ }
+
+ @Override
+ public LocalDate getBillingEndDate() {
+ final DateTime futureOrCurrentEndDateForSubscription = getSubscriptionBase().getEndDate() != null ? getSubscriptionBase().getEndDate() : getSubscriptionBase().getFutureEndDate();
+ final DateTime futureOrCurrentEndDateForBaseSubscription;
+ if (getBasePlanSubscriptionBase() == null) {
+ futureOrCurrentEndDateForBaseSubscription = null;
+ } else {
+ futureOrCurrentEndDateForBaseSubscription = getBasePlanSubscriptionBase().getEndDate() != null ? getBasePlanSubscriptionBase().getEndDate() : getBasePlanSubscriptionBase().getFutureEndDate();
+ }
+
+ final DateTime futureOrCurrentEndDate;
+ if (futureOrCurrentEndDateForBaseSubscription != null && futureOrCurrentEndDateForBaseSubscription.isBefore(futureOrCurrentEndDateForSubscription)) {
+ futureOrCurrentEndDate = futureOrCurrentEndDateForBaseSubscription;
+ } else {
+ futureOrCurrentEndDate = futureOrCurrentEndDateForSubscription;
+ }
+
+ return futureOrCurrentEndDate != null ? new LocalDate(futureOrCurrentEndDate, getAccountTimeZone()) : null;
+ }
+
+ @Override
+ public LocalDate getChargedThroughDate() {
+ return getSubscriptionBase().getChargedThroughDate() != null ? new LocalDate(getSubscriptionBase().getChargedThroughDate(), getAccountTimeZone()) : null;
+ }
+
+ @Override
+ public String getCurrentStateForService(final String serviceName) {
+ if (currentSubscriptionBlockingStatesForServices == null) {
+ return null;
+ } else {
+ final BlockingState blockingState = Iterables.<BlockingState>tryFind(currentSubscriptionBlockingStatesForServices,
+ new Predicate<BlockingState>() {
+ @Override
+ public boolean apply(final BlockingState input) {
+ return serviceName.equals(input.getService());
+ }
+ }).orNull();
+ return blockingState == null ? null : blockingState.getService();
+ }
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundle.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundle.java
new file mode 100644
index 0000000..6f0144d
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundle.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+public class DefaultSubscriptionBundle implements SubscriptionBundle {
+
+ private final UUID id;
+ private final UUID accountId;
+ private final String externalKey;
+ private final List<Subscription> subscriptions;
+ private final SubscriptionBundleTimeline bundleTimeline;
+ private final DateTime createdDate;
+ private final DateTime updatedDate;
+ private final DateTime originalCreatedDate;
+
+ public DefaultSubscriptionBundle(final UUID id, final UUID accountId, final String externalKey, final List<Subscription> subscriptions, final SubscriptionBundleTimeline bundleTimeline,
+ final DateTime originalCreatedDate, final DateTime createdDate, final DateTime updatedDate) {
+ this.id = id;
+ this.accountId = accountId;
+ this.externalKey = externalKey;
+ this.subscriptions = subscriptions;
+ this.bundleTimeline = bundleTimeline;
+ this.originalCreatedDate = originalCreatedDate;
+ this.createdDate = createdDate;
+ this.updatedDate = updatedDate;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ @Override
+ public DateTime getOriginalCreatedDate() {
+ return originalCreatedDate;
+ }
+
+ @Override
+ public List<Subscription> getSubscriptions() {
+ return subscriptions;
+ }
+
+ @Override
+ public SubscriptionBundleTimeline getTimeline() {
+ return bundleTimeline;
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return createdDate;
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return updatedDate;
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementDateHelper.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementDateHelper.java
new file mode 100644
index 0000000..e5ba8e1
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementDateHelper.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+
+public class EntitlementDateHelper {
+
+ private final AccountInternalApi accountApi;
+ private final Clock clock;
+
+ public EntitlementDateHelper(final AccountInternalApi accountApi, final Clock clock) {
+ this.accountApi = accountApi;
+ this.clock = clock;
+ }
+
+ public DateTime fromNowAndReferenceTime(final DateTime referenceDateTime, final InternalTenantContext callContext) throws EntitlementApiException {
+ try {
+ final Account account = accountApi.getAccountByRecordId(callContext.getAccountRecordId(), callContext);
+ return fromNowAndReferenceTime(referenceDateTime, account.getTimeZone());
+ } catch (AccountApiException e) {
+ throw new EntitlementApiException(e);
+ }
+ }
+
+ public DateTime fromNowAndReferenceTime(final DateTime referenceDateTime, final DateTimeZone accountTimeZone) {
+ final LocalDate localDateNowInAccountTimezone = new LocalDate(clock.getUTCNow(), accountTimeZone);
+ return fromLocalDateAndReferenceTime(localDateNowInAccountTimezone, referenceDateTime, accountTimeZone);
+ }
+
+ public DateTime fromLocalDateAndReferenceTime(final LocalDate requestedDate, final DateTime referenceDateTime, final InternalTenantContext callContext) throws EntitlementApiException {
+ try {
+ final Account account = accountApi.getAccountByRecordId(callContext.getAccountRecordId(), callContext);
+ return fromLocalDateAndReferenceTime(requestedDate, referenceDateTime, account.getTimeZone());
+ } catch (AccountApiException e) {
+ throw new EntitlementApiException(e);
+ }
+ }
+
+ public DateTime fromLocalDateAndReferenceTime(final LocalDate requestedDate, final DateTime referenceDateTime, final DateTimeZone accountTimeZone) {
+ final LocalDate localDateNowInAccountTimezone = new LocalDate(requestedDate, accountTimeZone);
+ // Datetime from local date in account timezone and with given reference time
+ final DateTime t1 = localDateNowInAccountTimezone.toDateTime(referenceDateTime.toLocalTime(), accountTimeZone);
+ // Datetime converted back in UTC
+ final DateTime t2 = new DateTime(t1, DateTimeZone.UTC);
+
+ //
+ // Ok, in the case of a LocalDate of today we expect any change to be immediate, so we check that DateTime returned is not in the future
+ // (which means that reference time might not be honored, but this is not very important).
+ //
+ return adjustDateTimeToNotBeInFutureIfLocaDateIsToday(t2, accountTimeZone);
+ }
+
+ private DateTime adjustDateTimeToNotBeInFutureIfLocaDateIsToday(final DateTime inputUtc, final DateTimeZone accountTimeZone) {
+ // If the LocalDate is TODAY but after adding the reference time we end up in the future, we correct it to be NOW,
+ // so change occurs immediately.
+ // If the LocalDate is TODAY but after adding the reference time we end up in the past, we also correct it to NOW,
+ // so we don't end up having events between this time and NOW.
+ // Note that in both these cases, we won't respect the reference time.
+ if (isEqualsToday(inputUtc, accountTimeZone)) {
+ return clock.getUTCNow();
+ } else {
+ return inputUtc;
+ }
+ }
+
+ /**
+ * Check if the date portion of a date/time is equals at today (as returned by the clock).
+ *
+ * @param inputDate the fully qualified DateTime
+ * @param accountTimeZone the account timezone
+ * @return true if the inputDate, once converted into a LocalDate using account timezone is equals at today
+ */
+ private boolean isEqualsToday(final DateTime inputDate, final DateTimeZone accountTimeZone) {
+ final LocalDate localDateNowInAccountTimezone = new LocalDate(clock.getUTCNow(), accountTimeZone);
+ final LocalDate targetDateInAccountTimezone = new LocalDate(inputDate, accountTimeZone);
+
+ return targetDateInAccountTimezone.compareTo(localDateNowInAccountTimezone) == 0;
+ }
+
+ /**
+ * Check if the date portion of a date/time is before or equals at now (as returned by the clock).
+ *
+ * @param inputDate the fully qualified DateTime
+ * @param accountTimeZone the account timezone
+ * @return true if the inputDate, once converted into a LocalDate using account timezone is less or equals than today
+ */
+ public boolean isBeforeOrEqualsToday(final DateTime inputDate, final DateTimeZone accountTimeZone) {
+ final LocalDate localDateNowInAccountTimezone = new LocalDate(clock.getUTCNow(), accountTimeZone);
+ final LocalDate targetDateInAccountTimezone = new LocalDate(inputDate, accountTimeZone);
+
+ return targetDateInAccountTimezone.compareTo(localDateNowInAccountTimezone) <= 0;
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEntitlements.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEntitlements.java
new file mode 100644
index 0000000..f86b587
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEntitlements.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api.svcs;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.entitlement.AccountEntitlements;
+import org.killbill.billing.entitlement.AccountEventsStreams;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+
+public class DefaultAccountEntitlements implements AccountEntitlements {
+
+ private final AccountEventsStreams accountEventsStreams;
+ private final Map<UUID, Collection<Entitlement>> entitlements;
+
+ public DefaultAccountEntitlements(final AccountEventsStreams accountEventsStreams, final Map<UUID, Collection<Entitlement>> entitlements) {
+ this.accountEventsStreams = accountEventsStreams;
+ this.entitlements = entitlements;
+ }
+
+ @Override
+ public Account getAccount() {
+ return accountEventsStreams.getAccount();
+ }
+
+ @Override
+ public Map<UUID, SubscriptionBaseBundle> getBundles() {
+ return accountEventsStreams.getBundles();
+ }
+
+ @Override
+ public Map<UUID, Collection<Entitlement>> getEntitlements() {
+ return entitlements;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultAccountEntitlements{");
+ sb.append("accountEventsStreams=").append(accountEventsStreams);
+ sb.append(", entitlements=").append(entitlements);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultAccountEntitlements that = (DefaultAccountEntitlements) o;
+
+ if (accountEventsStreams != null ? !accountEventsStreams.equals(that.accountEventsStreams) : that.accountEventsStreams != null) {
+ return false;
+ }
+ if (entitlements != null ? !entitlements.equals(that.entitlements) : that.entitlements != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = accountEventsStreams != null ? accountEventsStreams.hashCode() : 0;
+ result = 31 * result + (entitlements != null ? entitlements.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEventsStreams.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEventsStreams.java
new file mode 100644
index 0000000..df46e81
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultAccountEventsStreams.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api.svcs;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.entitlement.AccountEventsStreams;
+import org.killbill.billing.entitlement.EventsStream;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class DefaultAccountEventsStreams implements AccountEventsStreams {
+
+ private final Account account;
+ private final Map<UUID, Collection<EventsStream>> eventsStreams;
+ private final Map<UUID, SubscriptionBaseBundle> bundles = new HashMap<UUID, SubscriptionBaseBundle>();
+
+ public DefaultAccountEventsStreams(final Account account,
+ final Iterable<SubscriptionBaseBundle> bundles,
+ final Map<UUID, Collection<EventsStream>> eventsStreams) {
+ this.account = account;
+ this.eventsStreams = eventsStreams;
+ for (final SubscriptionBaseBundle baseBundle : bundles) {
+ this.bundles.put(baseBundle.getId(), baseBundle);
+ }
+ }
+
+ public DefaultAccountEventsStreams(final Account account) {
+ this(account, ImmutableList.<SubscriptionBaseBundle>of(), ImmutableMap.<UUID, Collection<EventsStream>>of());
+ }
+
+ @Override
+ public Account getAccount() {
+ return account;
+ }
+
+ @Override
+ public Map<UUID, SubscriptionBaseBundle> getBundles() {
+ return bundles;
+ }
+
+ @Override
+ public Map<UUID, Collection<EventsStream>> getEventsStreams() {
+ return eventsStreams;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultAccountEventsStreams{");
+ sb.append("account=").append(account);
+ sb.append(", eventsStreams=").append(eventsStreams);
+ sb.append(", bundles=").append(bundles);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultAccountEventsStreams that = (DefaultAccountEventsStreams) o;
+
+ if (account != null ? !account.equals(that.account) : that.account != null) {
+ return false;
+ }
+ if (bundles != null ? !bundles.equals(that.bundles) : that.bundles != null) {
+ return false;
+ }
+ if (eventsStreams != null ? !eventsStreams.equals(that.eventsStreams) : that.eventsStreams != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = account != null ? account.hashCode() : 0;
+ result = 31 * result + (eventsStreams != null ? eventsStreams.hashCode() : 0);
+ result = 31 * result + (bundles != null ? bundles.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
new file mode 100644
index 0000000..a0247ca
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api.svcs;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.AccountEntitlements;
+import org.killbill.billing.entitlement.AccountEventsStreams;
+import org.killbill.billing.entitlement.EntitlementInternalApi;
+import org.killbill.billing.entitlement.EventsStream;
+import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.EntitlementApi;
+import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.entitlement.api.EntitlementDateHelper;
+import org.killbill.billing.entitlement.block.BlockingChecker;
+import org.killbill.billing.entitlement.dao.BlockingStateDao;
+import org.killbill.billing.entitlement.engine.core.EntitlementUtils;
+import org.killbill.billing.entitlement.engine.core.EventsStreamBuilder;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+public class DefaultEntitlementInternalApi implements EntitlementInternalApi {
+
+ private final EntitlementApi entitlementApi;
+ private final SubscriptionBaseInternalApi subscriptionInternalApi;
+ private final Clock clock;
+ private final InternalCallContextFactory internalCallContextFactory;
+ private final BlockingChecker checker;
+ private final BlockingStateDao blockingStateDao;
+ private final EntitlementDateHelper dateHelper;
+ private final EventsStreamBuilder eventsStreamBuilder;
+ private final EntitlementUtils entitlementUtils;
+ private final NotificationQueueService notificationQueueService;
+
+ @Inject
+ public DefaultEntitlementInternalApi(final EntitlementApi entitlementApi, final InternalCallContextFactory internalCallContextFactory,
+ final SubscriptionBaseInternalApi subscriptionInternalApi,
+ final AccountInternalApi accountApi, final BlockingStateDao blockingStateDao, final Clock clock,
+ final BlockingChecker checker, final NotificationQueueService notificationQueueService,
+ final EventsStreamBuilder eventsStreamBuilder, final EntitlementUtils entitlementUtils) {
+ this.entitlementApi = entitlementApi;
+ this.internalCallContextFactory = internalCallContextFactory;
+ this.subscriptionInternalApi = subscriptionInternalApi;
+ this.clock = clock;
+ this.checker = checker;
+ this.blockingStateDao = blockingStateDao;
+ this.notificationQueueService = notificationQueueService;
+ this.eventsStreamBuilder = eventsStreamBuilder;
+ this.entitlementUtils = entitlementUtils;
+ this.dateHelper = new EntitlementDateHelper(accountApi, clock);
+ }
+
+ @Override
+ public AccountEntitlements getAllEntitlementsForAccountId(final UUID accountId, final TenantContext tenantContext) throws EntitlementApiException {
+ final InternalTenantContext context = internalCallContextFactory.createInternalTenantContext(accountId, tenantContext);
+
+ final AccountEventsStreams accountEventsStreams = eventsStreamBuilder.buildForAccount(context);
+
+ final Map<UUID, Collection<Entitlement>> entitlementsPerBundle = new HashMap<UUID, Collection<Entitlement>>();
+ for (final UUID bundleId : accountEventsStreams.getEventsStreams().keySet()) {
+ if (entitlementsPerBundle.get(bundleId) == null) {
+ entitlementsPerBundle.put(bundleId, new LinkedList<Entitlement>());
+ }
+
+ for (final EventsStream eventsStream : accountEventsStreams.getEventsStreams().get(bundleId)) {
+ final Entitlement entitlement = new DefaultEntitlement(eventsStream, eventsStreamBuilder, entitlementApi,
+ blockingStateDao, subscriptionInternalApi, checker, notificationQueueService,
+ entitlementUtils, dateHelper, clock, internalCallContextFactory);
+ entitlementsPerBundle.get(bundleId).add(entitlement);
+ }
+ }
+
+ return new DefaultAccountEntitlements(accountEventsStreams, entitlementsPerBundle);
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java
new file mode 100644
index 0000000..971786e
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api.svcs;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.dao.BlockingStateDao;
+import org.killbill.billing.entitlement.engine.core.EntitlementUtils;
+import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.junction.DefaultBlockingState;
+
+import com.google.inject.Inject;
+
+public class DefaultInternalBlockingApi implements BlockingInternalApi {
+
+ private final EntitlementUtils entitlementUtils;
+ private final BlockingStateDao dao;
+ private final Clock clock;
+
+ @Inject
+ public DefaultInternalBlockingApi(final EntitlementUtils entitlementUtils, final BlockingStateDao dao, final Clock clock) {
+ this.entitlementUtils = entitlementUtils;
+ this.dao = dao;
+ this.clock = clock;
+ }
+
+ @Override
+ public BlockingState getBlockingStateForService(final UUID overdueableId, final BlockingStateType blockingStateType, final String serviceName, final InternalTenantContext context) {
+ final BlockingState blockingStateForService = dao.getBlockingStateForService(overdueableId, blockingStateType, serviceName, context);
+ if (blockingStateForService == null) {
+ return DefaultBlockingState.getClearState(blockingStateType, serviceName, clock);
+ } else {
+ return blockingStateForService;
+ }
+ }
+
+ @Override
+ public List<BlockingState> getBlockingAllForAccount(final InternalTenantContext context) {
+ return dao.getBlockingAllForAccountRecordId(context);
+ }
+
+ @Override
+ public void setBlockingState(final BlockingState state, final InternalCallContext context) {
+ entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(state, context);
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/block/BlockingChecker.java b/entitlement/src/main/java/org/killbill/billing/entitlement/block/BlockingChecker.java
new file mode 100644
index 0000000..903df5a
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/block/BlockingChecker.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.block;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entitlement.api.Blockable;
+import org.killbill.billing.entitlement.api.BlockingApiException;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+
+public interface BlockingChecker {
+
+ public static final Object TYPE_SUBSCRIPTION = "Subscription";
+ public static final Object TYPE_BUNDLE = "Bundle";
+ public static final Object TYPE_ACCOUNT = "Account";
+
+ public static final Object ACTION_CHANGE = "Change";
+ public static final Object ACTION_ENTITLEMENT = "Entitlement";
+ public static final Object ACTION_BILLING = "Billing";
+
+ public interface BlockingAggregator {
+
+ public boolean isBlockChange();
+
+ public boolean isBlockEntitlement();
+
+ public boolean isBlockBilling();
+ }
+
+ public BlockingAggregator getBlockedStatus(List<BlockingState> currentAccountEntitlementStatePerService, List<BlockingState> currentBundleEntitlementStatePerService,
+ List<BlockingState> currentSubscriptionEntitlementStatePerService, InternalTenantContext internalTenantContext);
+
+ public BlockingAggregator getBlockedStatus(final UUID blockableId, final BlockingStateType type, final InternalTenantContext context) throws BlockingApiException;
+
+ public void checkBlockedChange(Blockable blockable, InternalTenantContext context) throws BlockingApiException;
+
+ public void checkBlockedEntitlement(Blockable blockable, InternalTenantContext context) throws BlockingApiException;
+
+ public void checkBlockedBilling(Blockable blockable, InternalTenantContext context) throws BlockingApiException;
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateDao.java b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateDao.java
new file mode 100644
index 0000000..c2d8c7f
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateDao.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.util.entity.dao.EntityDao;
+
+public interface BlockingStateDao extends EntityDao<BlockingStateModelDao, BlockingState, EntitlementApiException> {
+
+ /**
+ * Returns the current state for that specific service
+ *
+ * @param blockableId id of the blockable object
+ * @param blockingStateType blockable object type
+ * @param serviceName name of the service
+ * @param context call context
+ * @return current blocking state for that blockable object and service
+ */
+ public BlockingState getBlockingStateForService(UUID blockableId, BlockingStateType blockingStateType, String serviceName, InternalTenantContext context);
+
+ /**
+ * Returns the current state across all the services
+ *
+ * @param blockableId id of the blockable object
+ * @param blockingStateType blockable object type
+ * @param context call context
+ * @return list of current blocking states for that blockable object
+ */
+ public List<BlockingState> getBlockingState(UUID blockableId, BlockingStateType blockingStateType, InternalTenantContext context);
+
+ /**
+ * Return all events (past and future) across all services) for a given callcontext (account_record_id)
+ *
+ * @param context call context
+ * @return list of all blocking states for that account
+ */
+ public List<BlockingState> getBlockingAllForAccountRecordId(InternalTenantContext context);
+
+ /**
+ * Sets a new state for a specific service.
+ *
+ * @param state blocking state to set
+ * @param clock system clock
+ * @param context call context
+ */
+ public void setBlockingState(BlockingState state, Clock clock, InternalCallContext context);
+
+ /**
+ * Unactive the blocking state
+ *
+ * @param blockableId blockable id to unactivate
+ * @param context call context
+ */
+ public void unactiveBlockingState(UUID blockableId, final InternalCallContext context);
+
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateModelDao.java b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateModelDao.java
new file mode 100644
index 0000000..6305dd1
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateModelDao.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.dao;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class BlockingStateModelDao extends EntityBase implements EntityModelDao<BlockingState>{
+
+ private UUID blockableId;
+ private BlockingStateType type;
+ private String state;
+ private String service;
+ private Boolean blockChange;
+ private Boolean blockEntitlement;
+ private Boolean blockBilling;
+ private DateTime effectiveDate;
+ private boolean isActive;
+
+ public BlockingStateModelDao(final UUID id, final UUID blockableId, final BlockingStateType blockingStateType, final String state, final String service, final Boolean blockChange, final Boolean blockEntitlement,
+ final Boolean blockBilling, final DateTime effectiveDate, final boolean isActive, final DateTime createDate, final DateTime updateDate) {
+ super(id, createDate, updateDate);
+ this.blockableId = blockableId;
+ this.effectiveDate = effectiveDate;
+ this.type = blockingStateType;
+ this.state = state;
+ this.service = service;
+ this.blockChange = blockChange;
+ this.blockEntitlement = blockEntitlement;
+ this.blockBilling = blockBilling;
+ this.isActive = isActive;
+ }
+
+ public BlockingStateModelDao(final BlockingState src, final InternalCallContext context) {
+ this(src, context.getCreatedDate(), context.getUpdatedDate());
+ }
+
+ public BlockingStateModelDao(final BlockingState src, final DateTime createdDate, final DateTime updatedDate) {
+ this(src.getId(), src.getBlockedId(), src.getType(), src.getStateName(), src.getService(), src.isBlockChange(),
+ src.isBlockEntitlement(), src.isBlockBilling(), src.getEffectiveDate(), true, createdDate, updatedDate);
+ }
+
+ public UUID getBlockableId() {
+ return blockableId;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public String getService() {
+ return service;
+ }
+
+ public Boolean getBlockChange() {
+ return blockChange;
+ }
+
+ public Boolean getBlockEntitlement() {
+ return blockEntitlement;
+ }
+
+ public Boolean getBlockBilling() {
+ return blockBilling;
+ }
+
+ public BlockingStateType getType() {
+ return type;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public void setBlockableId(final UUID blockableId) {
+ this.blockableId = blockableId;
+ }
+
+ public void setType(final BlockingStateType type) {
+ this.type = type;
+ }
+
+ public void setState(final String state) {
+ this.state = state;
+ }
+
+ public void setService(final String service) {
+ this.service = service;
+ }
+
+ public void setBlockChange(final Boolean blockChange) {
+ this.blockChange = blockChange;
+ }
+
+ public void setBlockEntitlement(final Boolean blockEntitlement) {
+ this.blockEntitlement = blockEntitlement;
+ }
+
+ public void setBlockBilling(final Boolean blockBilling) {
+ this.blockBilling = blockBilling;
+ }
+
+ public void setEffectiveDate(final DateTime effectiveDate) {
+ this.effectiveDate = effectiveDate;
+ }
+
+ public void setIsActive(final boolean isActive) {
+ this.isActive = isActive;
+ }
+
+ // TODO required for jdbi binder
+ public boolean getIsActive() {
+ return isActive;
+ }
+
+ public boolean isActive() {
+ return isActive;
+ }
+
+ public static BlockingState toBlockingState(BlockingStateModelDao src) {
+ if (src == null) {
+ return null;
+ }
+ return new DefaultBlockingState(src.getId(), src.getBlockableId(), src.getType(), src.getState(), src.getService(), src.getBlockChange(), src.getBlockEntitlement(), src.getBlockBilling(),
+ src.getEffectiveDate(), src.getCreatedDate(), src.getUpdatedDate());
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.BLOCKING_STATES;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("BlockingStateModelDao");
+ sb.append("{blockableId=").append(blockableId);
+ sb.append(", state='").append(state).append('\'');
+ sb.append(", service='").append(service).append('\'');
+ sb.append(", blockChange=").append(blockChange);
+ sb.append(", blockEntitlement=").append(blockEntitlement);
+ sb.append(", blockBilling=").append(blockBilling);
+ sb.append(", isActive=").append(isActive);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateSqlDao.java b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateSqlDao.java
new file mode 100644
index 0000000..bdc1179
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/BlockingStateSqlDao.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.dao.MapperBase;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+@RegisterMapper(BlockingStateSqlDao.BlockingHistorySqlMapper.class)
+public interface BlockingStateSqlDao extends EntitySqlDao<BlockingStateModelDao, BlockingState> {
+
+ @SqlQuery
+ public abstract BlockingStateModelDao getBlockingStateForService(@Bind("blockableId") UUID blockableId,
+ @Bind("service") String serviceName,
+ @Bind("effectiveDate") Date effectiveDate,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public abstract List<BlockingStateModelDao> getBlockingState(@Bind("blockableId") UUID blockableId,
+ @Bind("effectiveDate") Date effectiveDate,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public abstract List<BlockingStateModelDao> getBlockingHistoryForService(@Bind("blockableId") UUID blockableId,
+ @Bind("service") String serviceName,
+ @BindBean final InternalTenantContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ public void unactiveEvent(@Bind("id") String id,
+ @BindBean final InternalCallContext context);
+
+ public class BlockingHistorySqlMapper extends MapperBase implements ResultSetMapper<BlockingStateModelDao> {
+
+ @Override
+ public BlockingStateModelDao map(final int index, final ResultSet r, final StatementContext ctx)
+ throws SQLException {
+
+ final UUID id;
+ final UUID blockableId;
+ final String stateName;
+ final String service;
+ final boolean blockChange;
+ final boolean blockEntitlement;
+ final boolean blockBilling;
+ final boolean isActive;
+ final DateTime effectiveDate;
+ final DateTime createdDate;
+ final BlockingStateType type;
+
+ id = UUID.fromString(r.getString("id"));
+ blockableId = UUID.fromString(r.getString("blockable_id"));
+ stateName = r.getString("state") == null ? DefaultBlockingState.CLEAR_STATE_NAME : r.getString("state");
+ service = r.getString("service");
+ type = BlockingStateType.valueOf(r.getString("type"));
+ blockChange = r.getBoolean("block_change");
+ blockEntitlement = r.getBoolean("block_entitlement");
+ blockBilling = r.getBoolean("block_billing");
+ isActive = r.getBoolean("is_active");
+ effectiveDate = getDateTime(r, "effective_date");
+ createdDate = getDateTime(r, "created_date");
+ return new BlockingStateModelDao(id, blockableId, type, stateName, service, blockChange, blockEntitlement, blockBilling, effectiveDate, isActive, createdDate, createdDate);
+ }
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/dao/OptimizedProxyBlockingStateDao.java b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/OptimizedProxyBlockingStateDao.java
new file mode 100644
index 0000000..1457e63
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/OptimizedProxyBlockingStateDao.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.dao;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.EventsStream;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.entitlement.engine.core.EventsStreamBuilder;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import com.google.common.collect.ImmutableList;
+
+public class OptimizedProxyBlockingStateDao extends ProxyBlockingStateDao {
+
+ public OptimizedProxyBlockingStateDao(final EventsStreamBuilder eventsStreamBuilder, final SubscriptionBaseInternalApi subscriptionBaseInternalApi,
+ final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher,
+ final NonEntityDao nonEntityDao) {
+ super(eventsStreamBuilder, subscriptionBaseInternalApi, dbi, clock, cacheControllerDispatcher, nonEntityDao);
+ }
+
+ /**
+ * Retrieve blocking states for a given subscription
+ * <p/>
+ * If the specified subscription is not an add-on, we already have the blocking states
+ * (they are all on disk) - we simply return them and there is nothing to do.
+ * Otherwise, for add-ons, we will need to compute the blocking states not on disk.
+ * <p/>
+ * This is a special method for EventsStreamBuilder to save some DAO calls.
+ *
+ * @param subscriptionBlockingStatesOnDisk
+ * blocking states on disk for that subscription
+ * @param allBlockingStatesOnDiskForAccount
+ * all blocking states on disk for that account
+ * @param account account associated with the subscription
+ * @param bundle bundle associated with the subscription
+ * @param baseSubscription base subscription (ProductCategory.BASE) associated with that bundle
+ * @param subscription subscription for which to build blocking states
+ * @param allSubscriptionsForBundle all subscriptions associated with that bundle
+ * @param context call context
+ * @return blocking states for that subscription
+ * @throws EntitlementApiException
+ */
+ public List<BlockingState> getBlockingHistory(final List<BlockingState> subscriptionBlockingStatesOnDisk,
+ final List<BlockingState> allBlockingStatesOnDiskForAccount,
+ final Account account,
+ final SubscriptionBaseBundle bundle,
+ @Nullable final SubscriptionBase baseSubscription,
+ final SubscriptionBase subscription,
+ final List<SubscriptionBase> allSubscriptionsForBundle,
+ final InternalTenantContext context) throws EntitlementApiException {
+ // blockable id points to a subscription, but make sure it's an add-on
+ if (!ProductCategory.ADD_ON.equals(subscription.getCategory())) {
+ // blockable id points to a base or standalone subscription, there is nothing to do
+ return subscriptionBlockingStatesOnDisk;
+ }
+
+ // Find all base entitlements that we care about (for which we want to find future cancelled add-ons)
+ final Iterable<EventsStream> eventsStreams = ImmutableList.<EventsStream>of(eventsStreamBuilder.buildForEntitlement(allBlockingStatesOnDiskForAccount,
+ account,
+ bundle,
+ baseSubscription,
+ allSubscriptionsForBundle,
+ context));
+
+ return addBlockingStatesNotOnDisk(subscription.getId(),
+ BlockingStateType.SUBSCRIPTION,
+ new LinkedList<BlockingState>(subscriptionBlockingStatesOnDisk),
+ ImmutableList.<SubscriptionBase>of(baseSubscription),
+ eventsStreams);
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/BlockingTransitionNotificationKey.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/BlockingTransitionNotificationKey.java
new file mode 100644
index 0000000..fc4b02d
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/BlockingTransitionNotificationKey.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.engine.core;
+
+import java.util.UUID;
+
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.notificationq.api.NotificationEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class BlockingTransitionNotificationKey implements NotificationEvent {
+
+ private final UUID blockingStateId;
+ private final UUID blockableId;
+ private final BlockingStateType blockingType;
+ private final Boolean isTransitionToBlockedBilling;
+ private final Boolean isTransitionToUnblockedBilling;
+ private final Boolean isTransitionToBlockedEntitlement;
+ private final Boolean isTransitionToUnblockedEntitlement;
+
+ @JsonCreator
+ public BlockingTransitionNotificationKey(@JsonProperty("blockingStateId") final UUID blockingStateId,
+ @JsonProperty("blockableId") final UUID blockableId,
+ @JsonProperty("type") final BlockingStateType blockingType,
+ @JsonProperty("isTransitionToBlockedBilling") final Boolean isTransitionToBlockedBilling,
+ @JsonProperty("isTransitionToUnblockedBilling") final Boolean isTransitionToUnblockedBilling,
+ @JsonProperty("isTransitionToBlockedEntitlement") final Boolean isTransitionToBlockedEntitlement,
+ @JsonProperty("isTransitionToUnblockedEntitlement") final Boolean isTransitionToUnblockedEntitlement) {
+
+ this.blockingStateId = blockingStateId;
+ this.blockableId = blockableId;
+ this.blockingType = blockingType;
+ this.isTransitionToBlockedBilling = isTransitionToBlockedBilling;
+ this.isTransitionToUnblockedBilling = isTransitionToUnblockedBilling;
+ this.isTransitionToBlockedEntitlement = isTransitionToBlockedEntitlement;
+ this.isTransitionToUnblockedEntitlement = isTransitionToUnblockedEntitlement;
+ }
+
+ public UUID getBlockingStateId() {
+ return blockingStateId;
+ }
+
+ public UUID getBlockableId() {
+ return blockableId;
+ }
+
+ public BlockingStateType getBlockingType() {
+ return blockingType;
+ }
+
+ @JsonProperty("isTransitionToBlockedBilling")
+ public Boolean isTransitionedToBlockedBilling() {
+ return isTransitionToBlockedBilling;
+ }
+
+ @JsonProperty("isTransitionToUnblockedBilling")
+ public Boolean isTransitionedToUnblockedBilling() {
+ return isTransitionToUnblockedBilling;
+ }
+
+ @JsonProperty("isTransitionToBlockedEntitlement")
+ public Boolean isTransitionedToBlockedEntitlement() {
+ return isTransitionToBlockedEntitlement;
+ }
+
+ @JsonProperty("isTransitionToUnblockedEntitlement")
+ public Boolean isTransitionToUnblockedEntitlement() {
+ return isTransitionToUnblockedEntitlement;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("BlockingTransitionNotificationKey{");
+ sb.append("blockingStateId=").append(blockingStateId);
+ sb.append(", blockableId=").append(blockableId);
+ sb.append(", blockingType=").append(blockingType);
+ sb.append(", isTransitionToBlockedBilling=").append(isTransitionToBlockedBilling);
+ sb.append(", isTransitionToUnblockedBilling=").append(isTransitionToUnblockedBilling);
+ sb.append(", isTransitionToBlockedEntitlement=").append(isTransitionToBlockedEntitlement);
+ sb.append(", isTransitionToUnblockedEntitlement=").append(isTransitionToUnblockedEntitlement);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final BlockingTransitionNotificationKey that = (BlockingTransitionNotificationKey) o;
+
+ if (blockingStateId != null ? !blockingStateId.equals(that.blockingStateId) : that.blockingStateId != null) {
+ return false;
+ }
+ if (blockableId != null ? !blockableId.equals(that.blockableId) : that.blockableId != null) {
+ return false;
+ }
+ if (blockingType != that.blockingType) {
+ return false;
+ }
+ if (isTransitionToBlockedBilling != null ? !isTransitionToBlockedBilling.equals(that.isTransitionToBlockedBilling) : that.isTransitionToBlockedBilling != null) {
+ return false;
+ }
+ if (isTransitionToBlockedEntitlement != null ? !isTransitionToBlockedEntitlement.equals(that.isTransitionToBlockedEntitlement) : that.isTransitionToBlockedEntitlement != null) {
+ return false;
+ }
+ if (isTransitionToUnblockedBilling != null ? !isTransitionToUnblockedBilling.equals(that.isTransitionToUnblockedBilling) : that.isTransitionToUnblockedBilling != null) {
+ return false;
+ }
+ if (isTransitionToUnblockedEntitlement != null ? !isTransitionToUnblockedEntitlement.equals(that.isTransitionToUnblockedEntitlement) : that.isTransitionToUnblockedEntitlement != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = blockingStateId != null ? blockingStateId.hashCode() : 0;
+ result = 31 * result + (blockableId != null ? blockableId.hashCode() : 0);
+ result = 31 * result + (blockingType != null ? blockingType.hashCode() : 0);
+ result = 31 * result + (isTransitionToBlockedBilling != null ? isTransitionToBlockedBilling.hashCode() : 0);
+ result = 31 * result + (isTransitionToUnblockedBilling != null ? isTransitionToUnblockedBilling.hashCode() : 0);
+ result = 31 * result + (isTransitionToBlockedEntitlement != null ? isTransitionToBlockedEntitlement.hashCode() : 0);
+ result = 31 * result + (isTransitionToUnblockedEntitlement != null ? isTransitionToUnblockedEntitlement.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EntitlementNotificationKey.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EntitlementNotificationKey.java
new file mode 100644
index 0000000..0bfe212
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EntitlementNotificationKey.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.engine.core;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.notificationq.api.NotificationEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class EntitlementNotificationKey implements NotificationEvent {
+
+ private final UUID entitlementId;
+ private final UUID bundleId;
+ private final EntitlementNotificationKeyAction entitlementNotificationKeyAction;
+ private final DateTime effectiveDate;
+
+ @JsonCreator
+ public EntitlementNotificationKey(@JsonProperty("entitlementId") final UUID entitlementId,
+ @JsonProperty("bundleId") final UUID bundleId,
+ @JsonProperty("entitlementNotificationKeyAction") final EntitlementNotificationKeyAction entitlementNotificationKeyAction,
+ @JsonProperty("effectiveDate") final DateTime effectiveDate) {
+ this.entitlementId = entitlementId;
+ this.bundleId = bundleId;
+ this.entitlementNotificationKeyAction = entitlementNotificationKeyAction;
+ this.effectiveDate = effectiveDate;
+ }
+
+ public UUID getEntitlementId() {
+ return entitlementId;
+ }
+
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ public EntitlementNotificationKeyAction getEntitlementNotificationKeyAction() {
+ return entitlementNotificationKeyAction;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("EntitlementNotificationKey{");
+ sb.append("entitlementId=").append(entitlementId);
+ sb.append(", bundleId=").append(bundleId);
+ sb.append(", entitlementNotificationKeyAction=").append(entitlementNotificationKeyAction);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final EntitlementNotificationKey that = (EntitlementNotificationKey) o;
+
+ if (entitlementId != null ? !entitlementId.equals(that.entitlementId) : that.entitlementId != null) {
+ return false;
+ }
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (entitlementNotificationKeyAction != that.entitlementNotificationKeyAction) {
+ return false;
+ }
+ if (effectiveDate != null ? effectiveDate.compareTo(that.effectiveDate) != 0 : that.effectiveDate != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = entitlementId != null ? entitlementId.hashCode() : 0;
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (entitlementNotificationKeyAction != null ? entitlementNotificationKeyAction.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EntitlementNotificationKeyAction.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EntitlementNotificationKeyAction.java
new file mode 100644
index 0000000..0d73520
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/EntitlementNotificationKeyAction.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.engine.core;
+
+public enum EntitlementNotificationKeyAction {
+ CANCEL,
+ CHANGE,
+ PAUSE,
+ RESUME
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementModule.java b/entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementModule.java
new file mode 100644
index 0000000..10d08ba
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/glue/DefaultEntitlementModule.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.entitlement.DefaultEntitlementService;
+import org.killbill.billing.entitlement.EntitlementInternalApi;
+import org.killbill.billing.entitlement.EntitlementService;
+import org.killbill.billing.entitlement.api.DefaultEntitlementApi;
+import org.killbill.billing.entitlement.api.DefaultSubscriptionApi;
+import org.killbill.billing.entitlement.api.EntitlementApi;
+import org.killbill.billing.entitlement.api.SubscriptionApi;
+import org.killbill.billing.entitlement.api.svcs.DefaultEntitlementInternalApi;
+import org.killbill.billing.entitlement.api.svcs.DefaultInternalBlockingApi;
+import org.killbill.billing.entitlement.block.BlockingChecker;
+import org.killbill.billing.entitlement.block.DefaultBlockingChecker;
+import org.killbill.billing.entitlement.dao.BlockingStateDao;
+import org.killbill.billing.entitlement.dao.ProxyBlockingStateDao;
+import org.killbill.billing.entitlement.engine.core.EntitlementUtils;
+import org.killbill.billing.entitlement.engine.core.EventsStreamBuilder;
+import org.killbill.billing.glue.EntitlementModule;
+import org.killbill.billing.junction.BlockingInternalApi;
+
+import com.google.inject.AbstractModule;
+
+public class DefaultEntitlementModule extends AbstractModule implements EntitlementModule {
+
+ public DefaultEntitlementModule(final ConfigSource configSource) {
+ }
+
+ @Override
+ protected void configure() {
+ installBlockingStateDao();
+ installBlockingApi();
+ installEntitlementApi();
+ installEntitlementInternalApi();
+ installSubscriptionApi();
+ installBlockingChecker();
+ bind(EntitlementService.class).to(DefaultEntitlementService.class).asEagerSingleton();
+ bind(EntitlementUtils.class).asEagerSingleton();
+ bind(EventsStreamBuilder.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installBlockingStateDao() {
+ bind(BlockingStateDao.class).to(ProxyBlockingStateDao.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installBlockingApi() {
+ bind(BlockingInternalApi.class).to(DefaultInternalBlockingApi.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installEntitlementApi() {
+ bind(EntitlementApi.class).to(DefaultEntitlementApi.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installEntitlementInternalApi() {
+ bind(EntitlementInternalApi.class).to(DefaultEntitlementInternalApi.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installSubscriptionApi() {
+ bind(SubscriptionApi.class).to(DefaultSubscriptionApi.class).asEagerSingleton();
+ }
+
+ public void installBlockingChecker() {
+ bind(BlockingChecker.class).to(DefaultBlockingChecker.class).asEagerSingleton();
+ }
+}
diff --git a/entitlement/src/main/resources/org/killbill/billing/entitlement/dao/BlockingStateSqlDao.sql.stg b/entitlement/src/main/resources/org/killbill/billing/entitlement/dao/BlockingStateSqlDao.sql.stg
new file mode 100644
index 0000000..62d8de4
--- /dev/null
+++ b/entitlement/src/main/resources/org/killbill/billing/entitlement/dao/BlockingStateSqlDao.sql.stg
@@ -0,0 +1,102 @@
+group BlockingStateSqlDao: EntitySqlDao;
+
+
+tableName() ::= "blocking_states"
+
+andCheckSoftDeletionWithComma(prefix) ::= "and <prefix>is_active"
+
+defaultOrderBy(prefix) ::= <<
+order by <prefix>effective_date ASC, <recordIdField(prefix)> ASC
+>>
+
+tableFields(prefix) ::= <<
+ <prefix>blockable_id
+, <prefix>type
+, <prefix>state
+, <prefix>service
+, <prefix>block_change
+, <prefix>block_entitlement
+, <prefix>block_billing
+, <prefix>effective_date
+, <prefix>is_active
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>updated_by
+, <prefix>updated_date
+>>
+
+
+tableValues() ::= <<
+ :blockableId
+, :type
+, :state
+, :service
+, :blockChange
+, :blockEntitlement
+, :blockBilling
+, :effectiveDate
+, :isActive
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+
+getBlockingStateForService() ::= <<
+select
+<allTableFields()>
+from
+<tableName()>
+where blockable_id = :blockableId
+and service = :service
+and effective_date \<= :effectiveDate
+and is_active
+<AND_CHECK_TENANT()>
+-- We want the current state, hence the order desc and limit 1
+order by effective_date desc, record_id desc
+limit 1
+;
+>>
+
+getBlockingState() ::= <<
+ select
+ <allTableFields("t.")>
+ from
+ <tableName()> t
+ join (
+ select max(record_id) record_id
+ , service
+ from blocking_states
+ where blockable_id = :blockableId
+ and effective_date \<= :effectiveDate
+ and is_active
+ <AND_CHECK_TENANT()>
+ group by service
+ ) tmp
+ on t.record_id = tmp.record_id
+ <defaultOrderBy("t.")>
+ ;
+ >>
+
+getBlockingHistoryForService() ::= <<
+select
+<allTableFields()>
+from
+<tableName()>
+where blockable_id = :blockableId
+and service = :service
+and is_active
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+unactiveEvent() ::= <<
+update
+<tableName()>
+set is_active = 0
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
diff --git a/entitlement/src/main/resources/org/killbill/billing/entitlement/ddl.sql b/entitlement/src/main/resources/org/killbill/billing/entitlement/ddl.sql
new file mode 100644
index 0000000..e44195e
--- /dev/null
+++ b/entitlement/src/main/resources/org/killbill/billing/entitlement/ddl.sql
@@ -0,0 +1,25 @@
+/*! SET storage_engine=INNODB */;
+
+DROP TABLE IF EXISTS blocking_states;
+CREATE TABLE blocking_states (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ blockable_id char(36) NOT NULL,
+ type varchar(20) NOT NULL,
+ state varchar(50) NOT NULL,
+ service varchar(20) NOT NULL,
+ block_change bool NOT NULL,
+ block_entitlement bool NOT NULL,
+ block_billing bool NOT NULL,
+ effective_date datetime NOT NULL,
+ is_active bool DEFAULT 1,
+ created_date datetime NOT NULL,
+ created_by varchar(50) NOT NULL,
+ updated_date datetime DEFAULT NULL,
+ updated_by varchar(50) DEFAULT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX blocking_states_id ON blocking_states(blockable_id);
+CREATE INDEX blocking_states_tenant_account_record_id ON blocking_states(tenant_record_id, account_record_id);
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEntitlementDateHelper.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEntitlementDateHelper.java
new file mode 100644
index 0000000..80d03dd
--- /dev/null
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEntitlementDateHelper.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entitlement.EntitlementTestSuiteNoDB;
+
+import static org.testng.Assert.assertTrue;
+
+public class TestEntitlementDateHelper extends EntitlementTestSuiteNoDB {
+
+ private Account account;
+ private EntitlementDateHelper dateHelper;
+
+ @BeforeClass(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeClass();
+
+
+ account = Mockito.mock(Account.class);
+ Mockito.when(accountInternalApi.getAccountByRecordId(Mockito.anyLong(), Mockito.<InternalTenantContext>any())).thenReturn(account);
+ dateHelper = new EntitlementDateHelper(accountInternalApi, clock);
+ clock.resetDeltaFromReality();;
+ }
+
+ @Test(groups = "fast")
+ public void testWithAccountInUtc() throws EntitlementApiException {
+
+ final LocalDate initialDate = new LocalDate(2013, 8, 7);
+ clock.setDay(initialDate.plusDays(1));
+
+ Mockito.when(account.getTimeZone()).thenReturn(DateTimeZone.UTC);
+
+ final DateTime refererenceDateTime = new DateTime(2013, 1, 1, 15, 43, 25, 0, DateTimeZone.UTC);
+ final DateTime targetDate = dateHelper.fromLocalDateAndReferenceTime(initialDate, refererenceDateTime, internalCallContext);
+ final DateTime expectedDate = new DateTime(2013, 8, 7, 15, 43, 25, 0, DateTimeZone.UTC);
+ Assert.assertEquals(targetDate, expectedDate);
+ }
+
+
+ @Test(groups = "fast")
+ public void testWithAccountInUtcMinus8() throws EntitlementApiException {
+
+ final LocalDate inputDate = new LocalDate(2013, 8, 7);
+ clock.setDay(inputDate.plusDays(3));
+
+ final DateTimeZone timeZoneUtcMinus8 = DateTimeZone.forOffsetHours(-8);
+ Mockito.when(account.getTimeZone()).thenReturn(timeZoneUtcMinus8);
+
+ // We also use a reference time of 1, 28, 10, 0 -> DateTime in accountTimeZone will be (2013, 8, 7, 1, 28, 10)
+ final DateTime refererenceDateTime = new DateTime(2013, 1, 1, 1, 28, 10, 0, DateTimeZone.UTC);
+ final DateTime targetDate = dateHelper.fromLocalDateAndReferenceTime(inputDate, refererenceDateTime, internalCallContext);
+
+ // And so that datetime in UTC becomes expectedDate below
+ final DateTime expectedDate = new DateTime(2013, 8, 7, 9, 28, 10, 0, DateTimeZone.UTC);
+ Assert.assertEquals(targetDate, expectedDate);
+ }
+
+
+ @Test(groups = "fast")
+ public void test2WithAccountInUtcMinus8() throws EntitlementApiException {
+
+ final DateTime initialNow = new DateTime(2013, 8, 22,22, 07, 01, 0, DateTimeZone.UTC);
+ clock.setTime(initialNow);
+
+ final LocalDate inputDate = new LocalDate(2013, 8, 22);
+
+ final DateTimeZone timeZoneUtcMinus8 = DateTimeZone.forOffsetHours(-8);
+ Mockito.when(account.getTimeZone()).thenReturn(timeZoneUtcMinus8);
+
+ // We also use a reference time of 16, 48, 0 -> DateTime in UTC will be (2013, 8, 23, 00, 48, 0) which:
+ // * is greater than now
+ // * with a inputLocalDate in the account timezone which is today
+ //
+ // => Code will round to now to not end up in the future
+ //
+ final DateTime refererenceDateTime = new DateTime(2013, 8, 22, 16, 48, 0, DateTimeZone.UTC);
+ final DateTime targetDate = dateHelper.fromLocalDateAndReferenceTime(inputDate, refererenceDateTime, internalCallContext);
+
+ final DateTime now = clock.getUTCNow();
+ Assert.assertTrue(initialNow.compareTo(targetDate) <= 0);
+ Assert.assertTrue(targetDate.compareTo(now) <= 0);
+ }
+
+
+
+ @Test(groups = "fast")
+ public void testWithAccountInUtcPlus5() throws EntitlementApiException {
+
+ final LocalDate inputDate = new LocalDate(2013, 8, 7);
+ clock.setDay(inputDate.plusDays(1));
+
+ final DateTimeZone timeZoneUtcPlus5 = DateTimeZone.forOffsetHours(+5);
+ Mockito.when(account.getTimeZone()).thenReturn(timeZoneUtcPlus5);
+
+ // We also use a reference time of 20, 28, 10, 0 -> DateTime in accountTimeZone will be (2013, 8, 7, 20, 28, 10)
+ final DateTime refererenceDateTime = new DateTime(2013, 1, 1, 20, 28, 10, 0, DateTimeZone.UTC);
+ final DateTime targetDate = dateHelper.fromLocalDateAndReferenceTime(inputDate, refererenceDateTime, internalCallContext);
+
+ // And so that datetime in UTC becomes expectedDate below
+ final DateTime expectedDate = new DateTime(2013, 8, 7, 15, 28, 10, 0, DateTimeZone.UTC);
+ Assert.assertEquals(targetDate, expectedDate);
+ }
+
+ @Test(groups = "fast")
+ public void testIsBeforeOrEqualsToday() {
+
+ clock.setTime(new DateTime(2013, 8, 7, 3, 28, 10, 0, DateTimeZone.UTC));
+ final DateTimeZone timeZoneUtcMinus8 = DateTimeZone.forOffsetHours(-8);
+
+
+ final DateTime inputDateEquals = new DateTime(2013, 8, 6, 23, 28, 10, 0, timeZoneUtcMinus8);
+ // Check that our input date is greater than now
+ assertTrue(inputDateEquals.compareTo(clock.getUTCNow()) > 0);
+ // And yet since the LocalDate match the function returns true
+ assertTrue(dateHelper.isBeforeOrEqualsToday(inputDateEquals, timeZoneUtcMinus8));
+ }
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEventJson.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEventJson.java
new file mode 100644
index 0000000..3422f32
--- /dev/null
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestEventJson.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.api;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.entitlement.EntitlementTestSuiteNoDB;
+import org.killbill.billing.events.BlockingTransitionInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+public class TestEventJson extends EntitlementTestSuiteNoDB {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Test(groups = "fast", description = "Test Blocking event deserialization")
+ public void testDefaultBlockingTransitionInternalEvent() throws Exception {
+ final BlockingTransitionInternalEvent e = new DefaultBlockingTransitionInternalEvent(UUID.randomUUID(), BlockingStateType.ACCOUNT, true, false, false, true, 1L, 2L, null);
+
+ final String json = mapper.writeValueAsString(e);
+
+ final Class<?> claz = Class.forName("org.killbill.billing.entitlement.api.DefaultBlockingTransitionInternalEvent");
+ final Object obj = mapper.readValue(json, claz);
+ Assert.assertTrue(obj.equals(e));
+ }
+
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/block/MockBlockingChecker.java b/entitlement/src/test/java/org/killbill/billing/entitlement/block/MockBlockingChecker.java
new file mode 100644
index 0000000..0213916
--- /dev/null
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/block/MockBlockingChecker.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.block;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entitlement.api.Blockable;
+import org.killbill.billing.entitlement.api.BlockingApiException;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+
+public class MockBlockingChecker implements BlockingChecker {
+
+ @Override
+ public BlockingAggregator getBlockedStatus(final List<BlockingState> accountEntitlementStates, final List<BlockingState> bundleEntitlementStates, final List<BlockingState> subscriptionEntitlementStates, final InternalTenantContext internalTenantContext) {
+ return null;
+ }
+
+ @Override
+ public BlockingAggregator getBlockedStatus(final UUID blockableId, final BlockingStateType type, final InternalTenantContext context) throws BlockingApiException {
+ return null;
+ }
+
+ @Override
+ public void checkBlockedChange(final Blockable blockable, final InternalTenantContext context) throws BlockingApiException {
+ }
+
+ @Override
+ public void checkBlockedEntitlement(final Blockable blockable, final InternalTenantContext context) throws BlockingApiException {
+ }
+
+ @Override
+ public void checkBlockedBilling(final Blockable blockable, final InternalTenantContext context) throws BlockingApiException {
+ }
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/block/TestBlockingApi.java b/entitlement/src/test/java/org/killbill/billing/entitlement/block/TestBlockingApi.java
new file mode 100644
index 0000000..e97bda7
--- /dev/null
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/block/TestBlockingApi.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.block;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.entitlement.EntitlementTestSuiteWithEmbeddedDB;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.UserType;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+public class TestBlockingApi extends EntitlementTestSuiteWithEmbeddedDB {
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ clock.resetDeltaFromReality();
+ }
+
+ @Test(groups = "slow")
+ public void testApi() {
+ final UUID uuid = UUID.randomUUID();
+ final String overdueStateName = "WayPassedItMan";
+ final String service = "TEST";
+
+ final boolean blockChange = true;
+ final boolean blockEntitlement = false;
+ final boolean blockBilling = false;
+
+ testListener.pushExpectedEvent(NextEvent.BLOCK);
+ final BlockingState state1 = new DefaultBlockingState(uuid, BlockingStateType.ACCOUNT, overdueStateName, service, blockChange, blockEntitlement, blockBilling, clock.getUTCNow());
+ blockingInternalApi.setBlockingState(state1, internalCallContext);
+ assertListenerStatus();
+
+ clock.setDeltaFromReality(1000 * 3600 * 24);
+
+ testListener.pushExpectedEvent(NextEvent.BLOCK);
+ final String overdueStateName2 = "NoReallyThisCantGoOn";
+ final BlockingState state2 = new DefaultBlockingState(uuid, BlockingStateType.ACCOUNT, overdueStateName2, service, blockChange, blockEntitlement, blockBilling, clock.getUTCNow());
+ blockingInternalApi.setBlockingState(state2, internalCallContext);
+ assertListenerStatus();
+
+ Assert.assertEquals(blockingInternalApi.getBlockingStateForService(uuid, BlockingStateType.ACCOUNT, service, internalCallContext).getStateName(), overdueStateName2);
+ }
+
+ @Test(groups = "slow")
+ public void testApiHistory() throws Exception {
+ final UUID uuid = UUID.randomUUID();
+ final String overdueStateName = "WayPassedItMan";
+ final String service = "TEST";
+
+ final boolean blockChange = true;
+ final boolean blockEntitlement = false;
+ final boolean blockBilling = false;
+
+ final Account account = accountApi.createAccount(getAccountData(7), callContext);
+ final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), "TestBlockingApi", CallOrigin.TEST, UserType.SYSTEM, UUID.randomUUID());
+
+ testListener.pushExpectedEvent(NextEvent.BLOCK);
+ final BlockingState state1 = new DefaultBlockingState(uuid, BlockingStateType.ACCOUNT, overdueStateName, service, blockChange, blockEntitlement, blockBilling, clock.getUTCNow());
+ blockingInternalApi.setBlockingState(state1, internalCallContext);
+ assertListenerStatus();
+
+ clock.setDeltaFromReality(1000 * 3600 * 24);
+
+ testListener.pushExpectedEvent(NextEvent.BLOCK);
+ final String overdueStateName2 = "NoReallyThisCantGoOn";
+ final BlockingState state2 = new DefaultBlockingState(uuid, BlockingStateType.ACCOUNT, overdueStateName2, service, blockChange, blockEntitlement, blockBilling, clock.getUTCNow());
+ blockingInternalApi.setBlockingState(state2, internalCallContext);
+ assertListenerStatus();
+
+ final List<BlockingState> blockingAll = blockingInternalApi.getBlockingAllForAccount(internalCallContext);
+ final List<BlockingState> history = ImmutableList.<BlockingState>copyOf(Collections2.<BlockingState>filter(blockingAll,
+ new Predicate<BlockingState>() {
+ @Override
+ public boolean apply(final BlockingState input) {
+ return input.getService().equals(service);
+ }
+ }));
+
+ Assert.assertEquals(history.size(), 2);
+ Assert.assertEquals(history.get(0).getStateName(), overdueStateName);
+ Assert.assertEquals(history.get(1).getStateName(), overdueStateName2);
+ }
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/dao/MockBlockingStateDao.java b/entitlement/src/test/java/org/killbill/billing/entitlement/dao/MockBlockingStateDao.java
new file mode 100644
index 0000000..475f626
--- /dev/null
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/dao/MockBlockingStateDao.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.dao;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.util.entity.dao.MockEntityDaoBase;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+public class MockBlockingStateDao extends MockEntityDaoBase<BlockingStateModelDao, BlockingState, EntitlementApiException> implements BlockingStateDao {
+
+ private final Map<UUID, List<BlockingState>> blockingStates = new HashMap<UUID, List<BlockingState>>();
+ private final Map<Long, List<BlockingState>> blockingStatesPerAccountRecordId = new HashMap<Long, List<BlockingState>>();
+
+ // TODO This mock class should also check that events are past or present
+
+ @Override
+ public BlockingState getBlockingStateForService(final UUID blockableId, final BlockingStateType blockingStateType, final String serviceName, final InternalTenantContext context) {
+ final List<BlockingState> states = blockingStates.get(blockableId);
+ if (states == null) {
+ return null;
+ }
+ final ImmutableList<BlockingState> filtered = ImmutableList.<BlockingState>copyOf(Collections2.filter(states, new Predicate<BlockingState>() {
+ @Override
+ public boolean apply(@Nullable final BlockingState input) {
+ return input.getService().equals(serviceName);
+ }
+ }));
+ return filtered.size() == 0 ? null : filtered.get(filtered.size() - 1);
+ }
+
+ @Override
+ public List<BlockingState> getBlockingState(final UUID blockableId, final BlockingStateType blockingStateType, final InternalTenantContext context) {
+ final List<BlockingState> blockingStatesForId = blockingStates.get(blockableId);
+ if (blockingStatesForId == null) {
+ return new ArrayList<BlockingState>();
+ }
+
+ final Map<String, BlockingState> tmp = new HashMap<String, BlockingState>();
+ for (BlockingState cur : blockingStatesForId) {
+ final BlockingState curStateForService = tmp.get(cur.getService());
+ if (curStateForService == null || curStateForService.getEffectiveDate().compareTo(cur.getEffectiveDate()) < 0) {
+ tmp.put(cur.getService(), cur);
+ }
+ }
+ return new ArrayList<BlockingState>(tmp.values());
+ }
+
+ @Override
+ public List<BlockingState> getBlockingAllForAccountRecordId(final InternalTenantContext context) {
+ return Objects.firstNonNull(blockingStatesPerAccountRecordId.get(context.getAccountRecordId()), ImmutableList.<BlockingState>of());
+ }
+
+ @Override
+ public synchronized void setBlockingState(final BlockingState state, final Clock clock, final InternalCallContext context) {
+ if (blockingStates.get(state.getBlockedId()) == null) {
+ blockingStates.put(state.getBlockedId(), new ArrayList<BlockingState>());
+ }
+ blockingStates.get(state.getBlockedId()).add(state);
+
+ if (blockingStatesPerAccountRecordId.get(context.getAccountRecordId()) == null) {
+ blockingStatesPerAccountRecordId.put(context.getAccountRecordId(), new ArrayList<BlockingState>());
+ }
+ blockingStatesPerAccountRecordId.get(context.getAccountRecordId()).add(state);
+ }
+
+ @Override
+ public void unactiveBlockingState(final UUID blockableId, final InternalCallContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ public synchronized void clear() {
+ blockingStates.clear();
+ blockingStatesPerAccountRecordId.clear();
+ }
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestBlockingDao.java b/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestBlockingDao.java
new file mode 100644
index 0000000..c78fd59
--- /dev/null
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestBlockingDao.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.entitlement.EntitlementTestSuiteWithEmbeddedDB;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.junction.DefaultBlockingState;
+
+public class TestBlockingDao extends EntitlementTestSuiteWithEmbeddedDB {
+
+ @BeforeMethod(groups = "slow")
+ public void setUp() throws Exception {
+ final Account account = accountApi.createAccount(getAccountData(7), callContext);
+
+ // Override the context with the right account record id
+ internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+ }
+
+ @Test(groups = "slow", description = "Check BlockingStateDao with a single service")
+ public void testDaoWithOneService() {
+ final UUID uuid = UUID.randomUUID();
+ final String overdueStateName = "WayPassedItMan";
+ final String service = "TEST";
+
+ final boolean blockChange = true;
+ final boolean blockEntitlement = false;
+ final boolean blockBilling = false;
+
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final BlockingState state1 = new DefaultBlockingState(uuid, BlockingStateType.ACCOUNT, overdueStateName, service, blockChange, blockEntitlement, blockBilling, clock.getUTCNow());
+ blockingStateDao.setBlockingState(state1, clock, internalCallContext);
+
+ clock.addDays(1);
+
+ final String overdueStateName2 = "NoReallyThisCantGoOn";
+ final BlockingState state2 = new DefaultBlockingState(uuid, BlockingStateType.ACCOUNT, overdueStateName2, service, blockChange, blockEntitlement, blockBilling, clock.getUTCNow());
+ blockingStateDao.setBlockingState(state2, clock, internalCallContext);
+
+ Assert.assertEquals(blockingStateDao.getBlockingStateForService(uuid, BlockingStateType.ACCOUNT, service, internalCallContext).getStateName(), state2.getStateName());
+
+ final List<BlockingState> states = blockingStateDao.getBlockingAllForAccountRecordId(internalCallContext);
+ Assert.assertEquals(states.size(), 2);
+
+ Assert.assertEquals(states.get(0).getStateName(), overdueStateName);
+ Assert.assertEquals(states.get(1).getStateName(), overdueStateName2);
+ }
+
+ @Test(groups = "slow", description = "Check BlockingStateDao with multiple services")
+ public void testDaoWithMultipleServices() throws Exception {
+ final UUID uuid = UUID.randomUUID();
+ final String overdueStateName = "WayPassedItMan";
+ final String service1 = "TEST";
+
+ final boolean blockChange = true;
+ final boolean blockEntitlement = false;
+ final boolean blockBilling = false;
+
+ final BlockingState state1 = new DefaultBlockingState(uuid, BlockingStateType.ACCOUNT, overdueStateName, service1, blockChange, blockEntitlement, blockBilling, clock.getUTCNow());
+ blockingStateDao.setBlockingState(state1, clock, internalCallContext);
+ clock.setDeltaFromReality(1000 * 3600 * 24);
+
+ final String service2 = "TEST2";
+
+ final String overdueStateName2 = "NoReallyThisCantGoOn";
+ final BlockingState state2 = new DefaultBlockingState(uuid, BlockingStateType.ACCOUNT, overdueStateName2, service2, blockChange, blockEntitlement, blockBilling, clock.getUTCNow());
+ blockingStateDao.setBlockingState(state2, clock, internalCallContext);
+
+ final List<BlockingState> history2 = blockingStateDao.getBlockingAllForAccountRecordId(internalCallContext);
+ Assert.assertEquals(history2.size(), 2);
+ Assert.assertEquals(history2.get(0).getStateName(), overdueStateName);
+ Assert.assertEquals(history2.get(1).getStateName(), overdueStateName2);
+ }
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/EntitlementTestSuiteNoDB.java b/entitlement/src/test/java/org/killbill/billing/entitlement/EntitlementTestSuiteNoDB.java
new file mode 100644
index 0000000..d820e60
--- /dev/null
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/EntitlementTestSuiteNoDB.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.entitlement.block.BlockingChecker;
+import org.killbill.billing.entitlement.dao.BlockingStateDao;
+import org.killbill.billing.entitlement.glue.TestEntitlementModuleNoDB;
+import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.tag.dao.TagDao;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public abstract class EntitlementTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ @Inject
+ protected AccountInternalApi accountInternalApi;
+ @Inject
+ protected BlockingInternalApi blockingInternalApi;
+ @Inject
+ protected BlockingStateDao blockingStateDao;
+ @Inject
+ protected CatalogService catalogService;
+ @Inject
+ protected SubscriptionBaseInternalApi subscriptionInternalApi;
+ @Inject
+ protected PersistentBus bus;
+ @Inject
+ protected TagDao tagDao;
+ @Inject
+ protected TagInternalApi tagInternalApi;
+ @Inject
+ protected BlockingChecker blockingChecker;
+
+ @BeforeClass(groups = "fast")
+ protected void beforeClass() throws Exception {
+ final Injector injector = Guice.createInjector(new TestEntitlementModuleNoDB(configSource));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ bus.start();
+ }
+
+ @AfterMethod(groups = "fast")
+ public void afterMethod() {
+ bus.stop();
+ }
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModule.java b/entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModule.java
new file mode 100644
index 0000000..3a303b1
--- /dev/null
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModule.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.util.glue.CacheModule;
+import org.killbill.billing.util.glue.CallContextModule;
+
+public class TestEntitlementModule extends DefaultEntitlementModule {
+
+ final protected ConfigSource configSource;
+
+ public TestEntitlementModule(final ConfigSource configSource) {
+ super(configSource);
+ this.configSource = configSource;
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ install(new CacheModule(configSource));
+ install(new CallContextModule());
+ }
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModuleNoDB.java b/entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModuleNoDB.java
new file mode 100644
index 0000000..2277f26
--- /dev/null
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModuleNoDB.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+import org.killbill.billing.catalog.MockCatalogModule;
+import org.killbill.billing.entitlement.dao.BlockingStateDao;
+import org.killbill.billing.entitlement.dao.MockBlockingStateDao;
+import org.killbill.billing.mock.glue.MockAccountModule;
+import org.killbill.billing.mock.glue.MockNonEntityDaoModule;
+import org.killbill.billing.mock.glue.MockSubscriptionModule;
+import org.killbill.billing.mock.glue.MockTagModule;
+import org.killbill.notificationq.MockNotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueConfig;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.util.bus.InMemoryBusModule;
+
+import com.google.common.collect.ImmutableMap;
+
+public class TestEntitlementModuleNoDB extends TestEntitlementModule {
+
+ public TestEntitlementModuleNoDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ install(new GuicyKillbillTestNoDBModule());
+ install(new MockNonEntityDaoModule());
+ install(new InMemoryBusModule(configSource));
+ install(new MockTagModule());
+ install(new MockSubscriptionModule());
+ install(new MockCatalogModule());
+ install(new MockAccountModule());
+ installNotificationQueue();
+ }
+
+ @Override
+ public void installBlockingStateDao() {
+ bind(BlockingStateDao.class).to(MockBlockingStateDao.class).asEagerSingleton();
+ }
+
+ private void installNotificationQueue() {
+ bind(NotificationQueueService.class).to(MockNotificationQueueService.class).asEagerSingleton();
+ configureNotificationQueueConfig();
+ }
+
+ protected void configureNotificationQueueConfig() {
+ final NotificationQueueConfig config = new ConfigurationObjectFactory(configSource).buildWithReplacements(NotificationQueueConfig.class,
+ ImmutableMap.<String, String>of("instanceName", "main"));
+ bind(NotificationQueueConfig.class).toInstance(config);
+ }
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModuleWithEmbeddedDB.java b/entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModuleWithEmbeddedDB.java
new file mode 100644
index 0000000..0413ada
--- /dev/null
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/glue/TestEntitlementModuleWithEmbeddedDB.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.entitlement.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
+import org.killbill.billing.account.glue.DefaultAccountModule;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.catalog.glue.CatalogModule;
+import org.killbill.billing.subscription.glue.DefaultSubscriptionModule;
+import org.killbill.billing.util.glue.AuditModule;
+import org.killbill.billing.util.glue.BusModule;
+import org.killbill.billing.util.glue.MetricsModule;
+import org.killbill.billing.util.glue.NonEntityDaoModule;
+import org.killbill.billing.util.glue.NotificationQueueModule;
+import org.killbill.billing.util.glue.TagStoreModule;
+
+public class TestEntitlementModuleWithEmbeddedDB extends TestEntitlementModule {
+
+ public TestEntitlementModuleWithEmbeddedDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ install(new DefaultAccountModule(configSource));
+ install(new GuicyKillbillTestWithEmbeddedDBModule());
+ install(new NonEntityDaoModule());
+ install(new MetricsModule());
+ install(new BusModule(configSource));
+ install(new TagStoreModule());
+ install(new CatalogModule(configSource));
+ install(new NotificationQueueModule(configSource));
+ install(new DefaultSubscriptionModule(configSource));
+ install(new AuditModule());
+
+ bind(TestApiListener.class).asEagerSingleton();
+ }
+}
diff --git a/entitlement/src/test/resources/entitlement.properties b/entitlement/src/test/resources/entitlement.properties
index 87d47cb..11f0313 100644
--- a/entitlement/src/test/resources/entitlement.properties
+++ b/entitlement/src/test/resources/entitlement.properties
@@ -1,5 +1,5 @@
-killbill.catalog.uri=file:src/test/resources/catalog.xml
-killbill.billing.persistent.bus.main.sleep=100
-killbill.billing.persistent.bus.main.nbThreads=1
-killbill.billing.persistent.bus.main.claimed=1
+org.killbill.catalog.uri=file:src/test/resources/catalog.xml
+org.killbill.persistent.bus.main.sleep=100
+org.killbill.persistent.bus.main.nbThreads=1
+org.killbill.persistent.bus.main.claimed=1
user.timezone=UTC
invoice/pom.xml 80(+30 -50)
diff --git a/invoice/pom.xml b/invoice/pom.xml
index c841124..e5081cc 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-invoice</artifactId>
@@ -46,97 +46,77 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.antlr</groupId>
+ <artifactId>stringtemplate</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-account</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>javax.inject</groupId>
- <artifactId>javax.inject</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.antlr</groupId>
- <artifactId>stringtemplate</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java b/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java
new file mode 100644
index 0000000..0be1fc0
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/DefaultInvoiceService.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api;
+
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.invoice.InvoiceListener;
+import org.killbill.billing.invoice.InvoiceTagHandler;
+import org.killbill.billing.invoice.notification.NextBillingDateNotifier;
+import org.killbill.billing.lifecycle.LifecycleHandlerType;
+import org.killbill.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists;
+
+import com.google.inject.Inject;
+
+public class DefaultInvoiceService implements InvoiceService {
+
+ public static final String INVOICE_SERVICE_NAME = "invoice-service";
+ private final NextBillingDateNotifier dateNotifier;
+ private final InvoiceListener invoiceListener;
+ private final InvoiceTagHandler tagHandler;
+ private final PersistentBus eventBus;
+
+ @Inject
+ public DefaultInvoiceService(final InvoiceListener invoiceListener, final InvoiceTagHandler tagHandler, final PersistentBus eventBus, final NextBillingDateNotifier dateNotifier) {
+ this.invoiceListener = invoiceListener;
+ this.tagHandler = tagHandler;
+ this.eventBus = eventBus;
+ this.dateNotifier = dateNotifier;
+ }
+
+ @Override
+ public String getName() {
+ return INVOICE_SERVICE_NAME;
+ }
+
+ @LifecycleHandlerType(LifecycleHandlerType.LifecycleLevel.INIT_SERVICE)
+ public void initialize() throws NotificationQueueAlreadyExists {
+ try {
+ eventBus.register(invoiceListener);
+ eventBus.register(tagHandler);
+ } catch (PersistentBus.EventBusException e) {
+ throw new RuntimeException("Unable to register to the EventBus!", e);
+ }
+ dateNotifier.initialize();
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.START_SERVICE)
+ public void start() {
+ dateNotifier.start();
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE)
+ public void stop() throws NoSuchNotificationQueue {
+ try {
+ eventBus.unregister(invoiceListener);
+ eventBus.unregister(tagHandler);
+ } catch (PersistentBus.EventBusException e) {
+ throw new RuntimeException("Unable to unregister to the EventBus!", e);
+ }
+ dateNotifier.stop();
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
new file mode 100644
index 0000000..7949310
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.invoice;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentApi;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
+import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.dao.InvoiceModelDao;
+import org.killbill.billing.invoice.dao.InvoicePaymentModelDao;
+import org.killbill.billing.invoice.model.DefaultInvoice;
+import org.killbill.billing.invoice.model.DefaultInvoicePayment;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+public class DefaultInvoicePaymentApi implements InvoicePaymentApi {
+
+ private final InvoiceDao dao;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public DefaultInvoicePaymentApi(final InvoiceDao dao, final InternalCallContextFactory internalCallContextFactory) {
+ this.dao = dao;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public List<Invoice> getAllInvoicesByAccount(final UUID accountId, final TenantContext context) {
+ return ImmutableList.<Invoice>copyOf(Collections2.transform(dao.getAllInvoicesByAccount(internalCallContextFactory.createInternalTenantContext(accountId, context)),
+ new Function<InvoiceModelDao, Invoice>() {
+ @Override
+ public Invoice apply(final InvoiceModelDao input) {
+ return new DefaultInvoice(input);
+ }
+ }));
+ }
+
+ @Override
+ public Invoice getInvoice(final UUID invoiceId, final TenantContext context) throws InvoiceApiException {
+ return new DefaultInvoice(dao.getById(invoiceId, internalCallContextFactory.createInternalTenantContext(invoiceId, ObjectType.INVOICE, context)));
+ }
+
+ @Override
+ public List<InvoicePayment> getInvoicePayments(final UUID paymentId, final TenantContext context) {
+ return ImmutableList.<InvoicePayment>copyOf(Collections2.transform(dao.getInvoicePayments(paymentId, internalCallContextFactory.createInternalTenantContext(paymentId, ObjectType.PAYMENT, context)),
+ new Function<InvoicePaymentModelDao, InvoicePayment>() {
+ @Override
+ public InvoicePayment apply(final InvoicePaymentModelDao input) {
+ return new DefaultInvoicePayment(input);
+ }
+ }));
+ }
+
+ @Override
+ public InvoicePayment getInvoicePaymentForAttempt(final UUID paymentId, final TenantContext context) {
+ final List<InvoicePayment> invoicePayments = getInvoicePayments(paymentId, context);
+ if (invoicePayments.size() == 0) {
+ return null;
+ }
+ return Collections2.filter(invoicePayments, new Predicate<InvoicePayment>() {
+ @Override
+ public boolean apply(final InvoicePayment input) {
+ return input.getType() == InvoicePaymentType.ATTEMPT;
+ }
+ }).iterator().next();
+ }
+
+ @Override
+ public BigDecimal getRemainingAmountPaid(final UUID invoicePaymentId, final TenantContext context) {
+ return dao.getRemainingAmountPaid(invoicePaymentId, internalCallContextFactory.createInternalTenantContext(invoicePaymentId, ObjectType.INVOICE_PAYMENT, context));
+ }
+
+ @Override
+ public List<InvoicePayment> getChargebacksByAccountId(final UUID accountId, final TenantContext context) {
+ return ImmutableList.<InvoicePayment>copyOf(Collections2.transform(dao.getChargebacksByAccountId(accountId, internalCallContextFactory.createInternalTenantContext(accountId, context)),
+ new Function<InvoicePaymentModelDao, InvoicePayment>() {
+ @Override
+ public InvoicePayment apply(final InvoicePaymentModelDao input) {
+ return new DefaultInvoicePayment(input);
+ }
+ }));
+ }
+
+ @Override
+ public List<InvoicePayment> getChargebacksByPaymentId(final UUID paymentId, final TenantContext context) {
+ return ImmutableList.<InvoicePayment>copyOf(Collections2.transform(dao.getChargebacksByPaymentId(paymentId, internalCallContextFactory.createInternalTenantContext(paymentId, ObjectType.PAYMENT, context)),
+ new Function<InvoicePaymentModelDao, InvoicePayment>() {
+ @Override
+ public InvoicePayment apply(final InvoicePaymentModelDao input) {
+ return new DefaultInvoicePayment(input);
+ }
+ }));
+ }
+
+ @Override
+ public InvoicePayment getChargebackById(final UUID chargebackId, final TenantContext context) throws InvoiceApiException {
+ return new DefaultInvoicePayment(dao.getChargebackById(chargebackId, internalCallContextFactory.createInternalTenantContext(chargebackId, ObjectType.INVOICE_PAYMENT, context)));
+ }
+
+ @Override
+ public UUID getAccountIdFromInvoicePaymentId(final UUID invoicePaymentId, final TenantContext context) throws InvoiceApiException {
+ return dao.getAccountIdFromInvoicePaymentId(invoicePaymentId, internalCallContextFactory.createInternalTenantContext(invoicePaymentId, ObjectType.INVOICE_PAYMENT, context));
+ }
+
+ @Override
+ public InvoicePayment createChargeback(final UUID invoicePaymentId, final CallContext context) throws InvoiceApiException {
+ return createChargeback(invoicePaymentId, null, context);
+ }
+
+ @Override
+ public InvoicePayment createChargeback(final UUID invoicePaymentId, final BigDecimal amount, final CallContext context) throws InvoiceApiException {
+ return new DefaultInvoicePayment(dao.postChargeback(invoicePaymentId, amount, internalCallContextFactory.createInternalCallContext(invoicePaymentId, ObjectType.INVOICE_PAYMENT, context)));
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
new file mode 100644
index 0000000..42dabfd
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.migration;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.MigrationPlan;
+import org.killbill.clock.Clock;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.api.InvoiceMigrationApi;
+import org.killbill.billing.invoice.dao.DefaultInvoiceDao;
+import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
+import org.killbill.billing.invoice.dao.InvoiceModelDao;
+import org.killbill.billing.invoice.dao.InvoicePaymentModelDao;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.account.api.AccountInternalApi;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+
+public class DefaultInvoiceMigrationApi implements InvoiceMigrationApi {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceMigrationApi.class);
+
+ private final AccountInternalApi accountUserApi;
+ private final DefaultInvoiceDao dao;
+ private final Clock clock;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public DefaultInvoiceMigrationApi(final AccountInternalApi accountUserApi,
+ final DefaultInvoiceDao dao,
+ final Clock clock,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.accountUserApi = accountUserApi;
+ this.dao = dao;
+ this.clock = clock;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public UUID createMigrationInvoice(final UUID accountId, final LocalDate targetDate, final BigDecimal balance, final Currency currency, final CallContext context) {
+ try {
+ accountUserApi.getAccountById(accountId, internalCallContextFactory.createInternalTenantContext(accountId, context));
+ } catch (AccountApiException e) {
+ log.warn("Unable to find account for id {}", accountId);
+ return null;
+ }
+
+ final InvoiceModelDao migrationInvoice = new InvoiceModelDao(accountId, clock.getUTCToday(), targetDate, currency, true);
+ final InvoiceItemModelDao migrationInvoiceItem = new InvoiceItemModelDao(context.getCreatedDate(), InvoiceItemType.FIXED, migrationInvoice.getId(), accountId, null, null,
+ MigrationPlan.MIGRATION_PLAN_NAME, MigrationPlan.MIGRATION_PLAN_PHASE_NAME,
+ targetDate, null, balance, null, currency, null);
+ dao.createInvoice(migrationInvoice, ImmutableList.<InvoiceItemModelDao>of(migrationInvoiceItem),
+ ImmutableList.<InvoicePaymentModelDao>of(), true, ImmutableMap.<UUID, DateTime>of(), internalCallContextFactory.createInternalCallContext(accountId, context));
+
+ return migrationInvoice.getId();
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/migration/MigrationInvoice.java b/invoice/src/main/java/org/killbill/billing/invoice/api/migration/MigrationInvoice.java
new file mode 100644
index 0000000..16832f6
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/migration/MigrationInvoice.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.migration;
+
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.model.DefaultInvoice;
+
+public class MigrationInvoice extends DefaultInvoice {
+
+ public MigrationInvoice(final UUID accountId, final LocalDate invoiceDate, final LocalDate targetDate, final Currency currency) {
+ super(UUID.randomUUID(), accountId, null, invoiceDate, targetDate, currency, true);
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
new file mode 100644
index 0000000..611b9d4
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.svcs;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
+import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.dao.InvoiceModelDao;
+import org.killbill.billing.invoice.dao.InvoicePaymentModelDao;
+import org.killbill.billing.invoice.model.DefaultInvoice;
+import org.killbill.billing.invoice.model.DefaultInvoicePayment;
+import org.killbill.billing.invoice.notification.NextBillingDatePoster;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.util.timezone.DateAndTimeZoneContext;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+
+public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceInternalApi.class);
+
+ private final InvoiceDao dao;
+ private final NextBillingDatePoster nextBillingDatePoster;
+ private final SubscriptionBaseInternalApi subscriptionBaseApi;
+ private final Clock clock;
+
+ @Inject
+ public DefaultInvoiceInternalApi(final InvoiceDao dao, final SubscriptionBaseInternalApi subscriptionBaseApi,
+ final Clock clock,
+ final NextBillingDatePoster nextBillingDatePoster) {
+ this.dao = dao;
+ this.clock = clock;
+ this.subscriptionBaseApi = subscriptionBaseApi;
+ this.nextBillingDatePoster = nextBillingDatePoster;
+ }
+
+ @Override
+ public Invoice getInvoiceById(final UUID invoiceId, final InternalTenantContext context) throws InvoiceApiException {
+ return new DefaultInvoice(dao.getById(invoiceId, context));
+ }
+
+ @Override
+ public Collection<Invoice> getUnpaidInvoicesByAccountId(final UUID accountId, final LocalDate upToDate, final InternalTenantContext context) {
+ return Collections2.transform(dao.getUnpaidInvoicesByAccountId(accountId, upToDate, context), new Function<InvoiceModelDao, Invoice>() {
+ @Override
+ public Invoice apply(final InvoiceModelDao input) {
+ return new DefaultInvoice(input);
+ }
+ });
+ }
+
+ @Override
+ public BigDecimal getAccountBalance(final UUID accountId, final InternalTenantContext context) {
+ return dao.getAccountBalance(accountId, context);
+ }
+
+ @Override
+ public void notifyOfPayment(final UUID invoiceId, final BigDecimal amount, final Currency currency, final Currency processedCurrency, final UUID paymentId, final DateTime paymentDate, final InternalCallContext context) throws InvoiceApiException {
+ final InvoicePayment invoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency);
+ notifyOfPayment(invoicePayment, context);
+ }
+
+ @Override
+ public void notifyOfPayment(final InvoicePayment invoicePayment, final InternalCallContext context) throws InvoiceApiException {
+ dao.notifyOfPayment(new InvoicePaymentModelDao(invoicePayment), context);
+ }
+
+ @Override
+ public InvoicePayment getInvoicePaymentForAttempt(final UUID paymentId, final InternalTenantContext context) throws InvoiceApiException {
+ final Collection<InvoicePayment> invoicePayments = Collections2.transform(dao.getInvoicePayments(paymentId, context), new Function<InvoicePaymentModelDao, InvoicePayment>() {
+ @Override
+ public InvoicePayment apply(final InvoicePaymentModelDao input) {
+ return new DefaultInvoicePayment(input);
+ }
+ });
+ if (invoicePayments.size() == 0) {
+ return null;
+ }
+ return Collections2.filter(invoicePayments, new Predicate<InvoicePayment>() {
+ @Override
+ public boolean apply(final InvoicePayment input) {
+ return input.getType() == InvoicePaymentType.ATTEMPT;
+ }
+ }).iterator().next();
+ }
+
+ @Override
+ public Invoice getInvoiceForPaymentId(final UUID paymentId, final InternalTenantContext context) throws InvoiceApiException {
+ final UUID invoiceIdStr = dao.getInvoiceIdByPaymentId(paymentId, context);
+ return invoiceIdStr == null ? null : new DefaultInvoice(dao.getById(invoiceIdStr, context));
+ }
+
+ @Override
+ public InvoicePayment createRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final UUID paymentCookieId, final InternalCallContext context) throws InvoiceApiException {
+ if (amount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new InvoiceApiException(ErrorCode.PAYMENT_REFUND_AMOUNT_NEGATIVE_OR_NULL);
+ }
+ return new DefaultInvoicePayment(dao.createRefund(paymentId, amount, isInvoiceAdjusted, invoiceItemIdsWithAmounts, paymentCookieId, context));
+ }
+
+ @Override
+ public void consumeExistingCBAOnAccountWithUnpaidInvoices(final UUID accountId, final InternalCallContext context) throws InvoiceApiException {
+ dao.consumeExstingCBAOnAccountWithUnpaidInvoices(accountId, context);
+ }
+
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceAdjustmentEvent.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceAdjustmentEvent.java
new file mode 100644
index 0000000..4ce5079
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceAdjustmentEvent.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.InvoiceAdjustmentInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultInvoiceAdjustmentEvent extends BusEventBase implements InvoiceAdjustmentInternalEvent {
+
+ private final UUID invoiceId;
+ private final UUID accountId;
+
+ @JsonCreator
+ public DefaultInvoiceAdjustmentEvent(@JsonProperty("invoiceId") final UUID invoiceId,
+ @JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.invoiceId = invoiceId;
+ this.accountId = accountId;
+ }
+
+ @Override
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.INVOICE_ADJUSTMENT;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultInvoiceAdjustmentEvent");
+ sb.append("{invoiceId=").append(invoiceId);
+ sb.append(", accountId=").append(accountId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultInvoiceAdjustmentEvent that = (DefaultInvoiceAdjustmentEvent) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = invoiceId != null ? invoiceId.hashCode() : 0;
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceCreationEvent.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceCreationEvent.java
new file mode 100644
index 0000000..c1a4d67
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceCreationEvent.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.user;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.InvoiceCreationInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultInvoiceCreationEvent extends BusEventBase implements InvoiceCreationInternalEvent {
+
+ private final UUID invoiceId;
+ private final UUID accountId;
+ private final BigDecimal amountOwed;
+ private final Currency currency;
+
+ @JsonCreator
+ public DefaultInvoiceCreationEvent(@JsonProperty("invoiceId") final UUID invoiceId,
+ @JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("amountOwed") final BigDecimal amountOwed,
+ @JsonProperty("currency") final Currency currency,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.invoiceId = invoiceId;
+ this.accountId = accountId;
+ this.amountOwed = amountOwed;
+ this.currency = currency;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.INVOICE_CREATION;
+ }
+
+ @Override
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public BigDecimal getAmountOwed() {
+ return amountOwed;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public String toString() {
+ return "DefaultInvoiceCreationNotification [invoiceId=" + invoiceId + ", accountId=" + accountId + ", amountOwed=" + amountOwed + ", currency=" + currency + "]";
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultInvoiceCreationEvent that = (DefaultInvoiceCreationEvent) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (amountOwed != null ? !amountOwed.equals(that.amountOwed) : that.amountOwed != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = invoiceId != null ? invoiceId.hashCode() : 0;
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (amountOwed != null ? amountOwed.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultNullInvoiceEvent.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultNullInvoiceEvent.java
new file mode 100644
index 0000000..cef9d2a
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultNullInvoiceEvent.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.user;
+
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.NullInvoiceInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultNullInvoiceEvent extends BusEventBase implements NullInvoiceInternalEvent {
+
+ private final UUID accountId;
+ private final LocalDate processingDate;
+
+ @JsonCreator
+ public DefaultNullInvoiceEvent(@JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("processingDate") final LocalDate processingDate,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.accountId = accountId;
+ this.processingDate = processingDate;
+
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.INVOICE_EMPTY;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public LocalDate getProcessingDate() {
+ return processingDate;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultNullInvoiceEvent");
+ sb.append("{accountId=").append(accountId);
+ sb.append(", processingDate=").append(processingDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((accountId == null) ? 0 : accountId.hashCode());
+ result = prime * result
+ + ((processingDate == null) ? 0 : processingDate.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final DefaultNullInvoiceEvent other = (DefaultNullInvoiceEvent) obj;
+ if (accountId == null) {
+ if (other.accountId != null) {
+ return false;
+ }
+ } else if (!accountId.equals(other.accountId)) {
+ return false;
+ }
+ if (processingDate == null) {
+ if (other.processingDate != null) {
+ return false;
+ }
+ } else if (processingDate.compareTo(other.processingDate) != 0) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java b/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
new file mode 100644
index 0000000..14ffe3f
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.calculator;
+
+import java.math.BigDecimal;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+public abstract class InvoiceCalculatorUtils {
+
+ // Invoice adjustments
+ public static boolean isInvoiceAdjustmentItem(final InvoiceItem invoiceItem, final Iterable<InvoiceItem> otherInvoiceItems) {
+ // Either REFUND_ADJ
+ return InvoiceItemType.REFUND_ADJ.equals(invoiceItem.getInvoiceItemType()) ||
+ // Or invoice level credit, i.e. credit adj, but NOT on its on own invoice
+ (InvoiceItemType.CREDIT_ADJ.equals(invoiceItem.getInvoiceItemType()) &&
+ !(Iterables.size(otherInvoiceItems) == 1 &&
+ InvoiceItemType.CBA_ADJ.equals(otherInvoiceItems.iterator().next().getInvoiceItemType()) &&
+ otherInvoiceItems.iterator().next().getInvoiceId().equals(invoiceItem.getInvoiceId()) &&
+ otherInvoiceItems.iterator().next().getAmount().compareTo(invoiceItem.getAmount().negate()) == 0));
+ }
+
+ // Item adjustments
+ public static boolean isInvoiceItemAdjustmentItem(final InvoiceItem invoiceItem) {
+ return InvoiceItemType.ITEM_ADJ.equals(invoiceItem.getInvoiceItemType()) || InvoiceItemType.REPAIR_ADJ.equals(invoiceItem.getInvoiceItemType());
+ }
+
+ // Account credits, gained or consumed
+ public static boolean isAccountCreditItem(final InvoiceItem invoiceItem) {
+ return InvoiceItemType.CBA_ADJ.equals(invoiceItem.getInvoiceItemType());
+ }
+
+ // Regular line item (charges)
+ public static boolean isCharge(final InvoiceItem invoiceItem) {
+ return InvoiceItemType.EXTERNAL_CHARGE.equals(invoiceItem.getInvoiceItemType()) ||
+ InvoiceItemType.FIXED.equals(invoiceItem.getInvoiceItemType()) ||
+ InvoiceItemType.RECURRING.equals(invoiceItem.getInvoiceItemType());
+ }
+
+ public static BigDecimal computeInvoiceBalance(final Currency currency,
+ @Nullable final Iterable<InvoiceItem> invoiceItems,
+ @Nullable final Iterable<InvoicePayment> invoicePayments) {
+ final BigDecimal invoiceBalance = computeInvoiceAmountCharged(currency, invoiceItems)
+ .add(computeInvoiceAmountCredited(currency, invoiceItems))
+ .add(computeInvoiceAmountAdjustedForAccountCredit(currency, invoiceItems))
+ .add(
+ computeInvoiceAmountPaid(currency, invoicePayments).negate()
+ .add(
+ computeInvoiceAmountRefunded(currency, invoicePayments).negate()
+ )
+ );
+
+ return KillBillMoney.of(invoiceBalance, currency);
+ }
+
+ // Snowflake for the CREDIT_ADJ on its own invoice
+ private static BigDecimal computeInvoiceAmountAdjustedForAccountCredit(final Currency currency, final Iterable<InvoiceItem> invoiceItems) {
+ BigDecimal amountAdjusted = BigDecimal.ZERO;
+ if (invoiceItems == null || !invoiceItems.iterator().hasNext()) {
+ return amountAdjusted;
+ }
+
+ for (final InvoiceItem invoiceItem : invoiceItems) {
+ final Iterable<InvoiceItem> otherInvoiceItems = Iterables.filter(invoiceItems, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return !input.getId().equals(invoiceItem.getId());
+ }
+ });
+
+ if (InvoiceItemType.CREDIT_ADJ.equals(invoiceItem.getInvoiceItemType()) &&
+ (Iterables.size(otherInvoiceItems) == 1 &&
+ InvoiceItemType.CBA_ADJ.equals(otherInvoiceItems.iterator().next().getInvoiceItemType()) &&
+ otherInvoiceItems.iterator().next().getInvoiceId().equals(invoiceItem.getInvoiceId()) &&
+ otherInvoiceItems.iterator().next().getAmount().compareTo(invoiceItem.getAmount().negate()) == 0)) {
+ amountAdjusted = amountAdjusted.add(invoiceItem.getAmount());
+ }
+ }
+
+ return KillBillMoney.of(amountAdjusted, currency);
+ }
+
+ public static BigDecimal computeInvoiceAmountCharged(final Currency currency, @Nullable final Iterable<InvoiceItem> invoiceItems) {
+ BigDecimal amountCharged = BigDecimal.ZERO;
+ if (invoiceItems == null || !invoiceItems.iterator().hasNext()) {
+ return amountCharged;
+ }
+
+ for (final InvoiceItem invoiceItem : invoiceItems) {
+ final Iterable<InvoiceItem> otherInvoiceItems = Iterables.filter(invoiceItems, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return !input.getId().equals(invoiceItem.getId());
+ }
+ });
+
+ if (isCharge(invoiceItem) ||
+ isInvoiceAdjustmentItem(invoiceItem, otherInvoiceItems) ||
+ isInvoiceItemAdjustmentItem(invoiceItem)) {
+ amountCharged = amountCharged.add(invoiceItem.getAmount());
+ }
+ }
+
+ return KillBillMoney.of(amountCharged, currency);
+ }
+
+ public static BigDecimal computeInvoiceOriginalAmountCharged(final DateTime invoiceCreatedDate, final Currency currency, @Nullable final Iterable<InvoiceItem> invoiceItems) {
+ BigDecimal amountCharged = BigDecimal.ZERO;
+ if (invoiceItems == null || !invoiceItems.iterator().hasNext()) {
+ return amountCharged;
+ }
+
+ for (final InvoiceItem invoiceItem : invoiceItems) {
+ if (isCharge(invoiceItem) &&
+ invoiceItem.getCreatedDate().equals(invoiceCreatedDate)) {
+ amountCharged = amountCharged.add(invoiceItem.getAmount());
+ }
+ }
+
+ return KillBillMoney.of(amountCharged, currency);
+ }
+
+ public static BigDecimal computeInvoiceAmountCredited(final Currency currency, @Nullable final Iterable<InvoiceItem> invoiceItems) {
+ BigDecimal amountCredited = BigDecimal.ZERO;
+ if (invoiceItems == null || !invoiceItems.iterator().hasNext()) {
+ return amountCredited;
+ }
+
+ for (final InvoiceItem invoiceItem : invoiceItems) {
+ if (isAccountCreditItem(invoiceItem)) {
+ amountCredited = amountCredited.add(invoiceItem.getAmount());
+ }
+ }
+
+ return KillBillMoney.of(amountCredited, currency);
+ }
+
+ public static BigDecimal computeInvoiceAmountPaid(final Currency currency, @Nullable final Iterable<InvoicePayment> invoicePayments) {
+ BigDecimal amountPaid = BigDecimal.ZERO;
+ if (invoicePayments == null || !invoicePayments.iterator().hasNext()) {
+ return amountPaid;
+ }
+
+ for (final InvoicePayment invoicePayment : invoicePayments) {
+ if (InvoicePaymentType.ATTEMPT.equals(invoicePayment.getType())) {
+ amountPaid = amountPaid.add(invoicePayment.getAmount());
+ }
+ }
+
+ return KillBillMoney.of(amountPaid, currency);
+ }
+
+ public static BigDecimal computeInvoiceAmountRefunded(final Currency currency, @Nullable final Iterable<InvoicePayment> invoicePayments) {
+ BigDecimal amountRefunded = BigDecimal.ZERO;
+ if (invoicePayments == null || !invoicePayments.iterator().hasNext()) {
+ return amountRefunded;
+ }
+
+ for (final InvoicePayment invoicePayment : invoicePayments) {
+ if (InvoicePaymentType.REFUND.equals(invoicePayment.getType()) ||
+ InvoicePaymentType.CHARGED_BACK.equals(invoicePayment.getType())) {
+ amountRefunded = amountRefunded.add(invoicePayment.getAmount());
+ }
+ }
+
+ return KillBillMoney.of(amountRefunded, currency);
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
new file mode 100644
index 0000000..82848fc
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entity.EntityPersistenceException;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.common.collect.Ordering;
+
+public class CBADao {
+
+ private final InvoiceDaoHelper invoiceDaoHelper;
+
+ public CBADao() {
+ this.invoiceDaoHelper = new InvoiceDaoHelper();
+ }
+
+
+ public BigDecimal getAccountCBAFromTransaction(final UUID accountId,
+ final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory,
+ final InternalTenantContext context) {
+ final List<InvoiceModelDao> invoices = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
+ return getAccountCBAFromTransaction(invoices);
+ }
+
+ public BigDecimal getAccountCBAFromTransaction(final List<InvoiceModelDao> invoices) {
+ BigDecimal cba = BigDecimal.ZERO;
+ for (final InvoiceModelDao cur : invoices) {
+ cba = cba.add(InvoiceModelDaoHelper.getCBAAmount(cur));
+ }
+ return cba;
+ }
+
+ public void doCBAComplexity(final UUID accountId, final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+
+ List<InvoiceModelDao> invoiceItemModelDaos = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
+ for (InvoiceModelDao cur : invoiceItemModelDaos) {
+ addCBAIfNeeded(entitySqlDaoWrapperFactory, cur, context);
+ }
+ invoiceItemModelDaos = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
+ useExistingCBAFromTransaction(invoiceItemModelDaos, entitySqlDaoWrapperFactory, context);
+ }
+
+ /**
+ * Adjust the invoice with a CBA item if the new invoice balance is negative.
+ *
+ * @param entitySqlDaoWrapperFactory the EntitySqlDaoWrapperFactory from the current transaction
+ * @param invoice the invoice to adjust
+ * @param context the call callcontext
+ */
+ private void addCBAIfNeeded(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory,
+ final InvoiceModelDao invoice,
+ final InternalCallContext context) throws EntityPersistenceException {
+
+ // If invoice balance becomes negative we add some CBA item
+ final BigDecimal balance = InvoiceModelDaoHelper.getBalance(invoice);
+ if (balance.compareTo(BigDecimal.ZERO) < 0) {
+ final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
+ final InvoiceItemModelDao cbaAdjItem = new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(), balance.negate(), invoice.getCurrency()));
+ transInvoiceItemDao.create(cbaAdjItem, context);
+ }
+ }
+
+
+ private void useExistingCBAFromTransaction(final List<InvoiceModelDao> invoices, final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final InternalCallContext context) throws InvoiceApiException, EntityPersistenceException {
+
+ final BigDecimal accountCBA = getAccountCBAFromTransaction(invoices);
+ if (accountCBA.compareTo(BigDecimal.ZERO) <= 0) {
+ return;
+ }
+
+ final List<InvoiceModelDao> unpaidInvoices = invoiceDaoHelper.getUnpaidInvoicesByAccountFromTransaction(invoices, null);
+ // We order the same os BillingStateCalculator-- should really share the comparator
+ final List<InvoiceModelDao> orderedUnpaidInvoices = Ordering.from(new Comparator<InvoiceModelDao>() {
+ @Override
+ public int compare(final InvoiceModelDao i1, final InvoiceModelDao i2) {
+ return i1.getInvoiceDate().compareTo(i2.getInvoiceDate());
+ }
+ }).immutableSortedCopy(unpaidInvoices);
+
+ BigDecimal remainingAccountCBA = accountCBA;
+ for (InvoiceModelDao cur : orderedUnpaidInvoices) {
+ final BigDecimal curInvoiceBalance = InvoiceModelDaoHelper.getBalance(cur);
+ final BigDecimal cbaToApplyOnInvoice = remainingAccountCBA.compareTo(curInvoiceBalance) <= 0 ? remainingAccountCBA : curInvoiceBalance;
+ remainingAccountCBA = remainingAccountCBA.subtract(cbaToApplyOnInvoice);
+
+ final InvoiceItemModelDao cbaAdjItem = new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(cur.getId(), cur.getAccountId(), context.getCreatedDate().toLocalDate(), cbaToApplyOnInvoice.negate(), cur.getCurrency()));
+
+ final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
+ transInvoiceItemDao.create(cbaAdjItem, context);
+
+ if (remainingAccountCBA.compareTo(BigDecimal.ZERO) <= 0) {
+ break;
+ }
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..8b8cd3f
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.EntityDao;
+
+public interface InvoiceDao extends EntityDao<InvoiceModelDao, Invoice, InvoiceApiException> {
+
+ void createInvoice(InvoiceModelDao invoice, List<InvoiceItemModelDao> invoiceItems,
+ List<InvoicePaymentModelDao> invoicePayments, boolean isRealInvoice, final Map<UUID, DateTime> callbackDateTimePerSubscriptions, InternalCallContext context);
+
+ InvoiceModelDao getByNumber(Integer number, InternalTenantContext context) throws InvoiceApiException;
+
+ List<InvoiceModelDao> getInvoicesByAccount(InternalTenantContext context);
+
+ List<InvoiceModelDao> getInvoicesByAccount(LocalDate fromDate, InternalTenantContext context);
+
+ List<InvoiceModelDao> getInvoicesBySubscription(UUID subscriptionId, InternalTenantContext context);
+
+ public Pagination<InvoiceModelDao> searchInvoices(String searchKey, Long offset, Long limit, InternalTenantContext context);
+
+ UUID getInvoiceIdByPaymentId(UUID paymentId, InternalTenantContext context);
+
+ List<InvoicePaymentModelDao> getInvoicePayments(UUID paymentId, InternalTenantContext context);
+
+ BigDecimal getAccountBalance(UUID accountId, InternalTenantContext context);
+
+ public BigDecimal getAccountCBA(UUID accountId, InternalTenantContext context);
+
+ List<InvoiceModelDao> getUnpaidInvoicesByAccountId(UUID accountId, @Nullable LocalDate upToDate, InternalTenantContext context);
+
+ // Include migrated invoices
+ List<InvoiceModelDao> getAllInvoicesByAccount(InternalTenantContext context);
+
+ InvoicePaymentModelDao postChargeback(UUID invoicePaymentId, BigDecimal amount, InternalCallContext context) throws InvoiceApiException;
+
+ /**
+ * Create a refund.
+ *
+ * @param paymentId payment associated with that refund
+ * @param amount amount to refund
+ * @param isInvoiceAdjusted whether the refund should trigger an invoice or invoice item adjustment
+ * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
+ * @param paymentCookieId payment cookie id
+ * @param context the call callcontext
+ * @return the created invoice payment object associated with this refund
+ * @throws InvoiceApiException
+ */
+ InvoicePaymentModelDao createRefund(UUID paymentId, BigDecimal amount, boolean isInvoiceAdjusted, Map<UUID, BigDecimal> invoiceItemIdsWithAmounts,
+ UUID paymentCookieId, InternalCallContext context) throws InvoiceApiException;
+
+ BigDecimal getRemainingAmountPaid(UUID invoicePaymentId, InternalTenantContext context);
+
+ UUID getAccountIdFromInvoicePaymentId(UUID invoicePaymentId, InternalTenantContext context) throws InvoiceApiException;
+
+ List<InvoicePaymentModelDao> getChargebacksByAccountId(UUID accountId, InternalTenantContext context);
+
+ List<InvoicePaymentModelDao> getChargebacksByPaymentId(UUID paymentId, InternalTenantContext context);
+
+ InvoicePaymentModelDao getChargebackById(UUID chargebackId, InternalTenantContext context) throws InvoiceApiException;
+
+ /**
+ * Retrieve am external charge by id.
+ *
+ * @param externalChargeId the external charge id
+ * @return the external charge invoice item
+ * @throws InvoiceApiException
+ */
+ InvoiceItemModelDao getExternalChargeById(UUID externalChargeId, InternalTenantContext context) throws InvoiceApiException;
+
+ /**
+ * Add an external charge to a given account and invoice. If invoiceId is null, a new invoice will be created.
+ *
+ * @param accountId the account id
+ * @param invoiceId the invoice id
+ * @param bundleId the bundle id
+ * @param description a description for that charge
+ * @param amount the external charge amount
+ * @param effectiveDate the day to post the external charge, in the account timezone
+ * @param currency the external charge currency
+ * @param context the call callcontext
+ * @return the newly created external charge invoice item
+ */
+ InvoiceItemModelDao insertExternalCharge(UUID accountId, @Nullable UUID invoiceId, @Nullable UUID bundleId, @Nullable String description,
+ BigDecimal amount, LocalDate effectiveDate, Currency currency, InternalCallContext context) throws InvoiceApiException;
+
+ /**
+ * Retrieve a credit by id.
+ *
+ * @param creditId the credit id
+ * @return the credit invoice item
+ * @throws InvoiceApiException
+ */
+ InvoiceItemModelDao getCreditById(UUID creditId, InternalTenantContext context) throws InvoiceApiException;
+
+ /**
+ * Add a credit to a given account and invoice. If invoiceId is null, a new invoice will be created.
+ *
+ * @param accountId the account id
+ * @param invoiceId the invoice id
+ * @param amount the credit amount
+ * @param effectiveDate the day to grant the credit, in the account timezone
+ * @param currency the credit currency
+ * @param context the call callcontext
+ * @return the newly created credit invoice item
+ */
+ InvoiceItemModelDao insertCredit(UUID accountId, @Nullable UUID invoiceId, BigDecimal amount,
+ LocalDate effectiveDate, Currency currency, InternalCallContext context);
+
+ /**
+ * Adjust an invoice item.
+ *
+ * @param accountId the account id
+ * @param invoiceId the invoice id
+ * @param invoiceItemId the invoice item id to adjust
+ * @param effectiveDate adjustment effective date, in the account timezone
+ * @param amount the amount to adjust. Pass null to adjust the full amount of the original item
+ * @param currency the currency of the amount. Pass null to default to the original currency used
+ * @param context the call callcontext
+ * @return the newly created adjustment item
+ */
+ InvoiceItemModelDao insertInvoiceItemAdjustment(UUID accountId, UUID invoiceId, UUID invoiceItemId, LocalDate effectiveDate,
+ @Nullable BigDecimal amount, @Nullable Currency currency, InternalCallContext context);
+
+ /**
+ * Delete a CBA item.
+ *
+ * @param accountId the account id
+ * @param invoiceId the invoice id
+ * @param invoiceItemId the invoice item id of the cba item to delete
+ */
+ void deleteCBA(UUID accountId, UUID invoiceId, UUID invoiceItemId, InternalCallContext context) throws InvoiceApiException;
+
+ void notifyOfPayment(InvoicePaymentModelDao invoicePayment, InternalCallContext context);
+
+ /**
+ * @param accountId the account for which we need to rebalance the CBA
+ * @param context the callcontext
+ */
+ public void consumeExstingCBAOnAccountWithUnpaidInvoices(final UUID accountId, final InternalCallContext context);
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java
new file mode 100644
index 0000000..2729793
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemModelDao.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class InvoiceItemModelDao extends EntityBase implements EntityModelDao<InvoiceItem> {
+
+ private InvoiceItemType type;
+ private UUID invoiceId;
+ private UUID accountId;
+ private UUID bundleId;
+ private UUID subscriptionId;
+ private String planName;
+ private String phaseName;
+ private LocalDate startDate;
+ private LocalDate endDate;
+ private BigDecimal amount;
+ private BigDecimal rate;
+ private Currency currency;
+ private UUID linkedItemId;
+
+ public InvoiceItemModelDao() { /* For the DAO mapper */ }
+
+ public InvoiceItemModelDao(final UUID id, final DateTime createdDate, final InvoiceItemType type, final UUID invoiceId,
+ final UUID accountId, final UUID bundleId, final UUID subscriptionId, final String planName,
+ final String phaseName, final LocalDate startDate, final LocalDate endDate, final BigDecimal amount,
+ final BigDecimal rate, final Currency currency, final UUID linkedItemId) {
+ super(id, createdDate, createdDate);
+ this.type = type;
+ this.invoiceId = invoiceId;
+ this.accountId = accountId;
+ this.bundleId = bundleId;
+ this.subscriptionId = subscriptionId;
+ this.planName = planName;
+ this.phaseName = phaseName;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.amount = amount;
+ this.rate = rate;
+ this.currency = currency;
+ this.linkedItemId = linkedItemId;
+ }
+
+ public InvoiceItemModelDao(final DateTime createdDate, final InvoiceItemType type, final UUID invoiceId, final UUID accountId,
+ final UUID bundleId, final UUID subscriptionId, final String planName,
+ final String phaseName, final LocalDate startDate, final LocalDate endDate, final BigDecimal amount,
+ final BigDecimal rate, final Currency currency, final UUID linkedItemId) {
+ this(UUID.randomUUID(), createdDate, type, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName,
+ startDate, endDate, amount, rate, currency, linkedItemId);
+ }
+
+ public InvoiceItemModelDao(final InvoiceItem invoiceItem) {
+ this(invoiceItem.getId(), invoiceItem.getCreatedDate(), invoiceItem.getInvoiceItemType(), invoiceItem.getInvoiceId(), invoiceItem.getAccountId(), invoiceItem.getBundleId(),
+ invoiceItem.getSubscriptionId(), invoiceItem.getPlanName(), invoiceItem.getPhaseName(), invoiceItem.getStartDate(), invoiceItem.getEndDate(),
+ invoiceItem.getAmount(), invoiceItem.getRate(), invoiceItem.getCurrency(), invoiceItem.getLinkedItemId());
+ }
+
+ public InvoiceItemType getType() {
+ return type;
+ }
+
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ public String getPlanName() {
+ return planName;
+ }
+
+ public String getPhaseName() {
+ return phaseName;
+ }
+
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+
+ public LocalDate getEndDate() {
+ return endDate;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public BigDecimal getRate() {
+ return rate;
+ }
+
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ public UUID getLinkedItemId() {
+ return linkedItemId;
+ }
+
+ public void setType(final InvoiceItemType type) {
+ this.type = type;
+ }
+
+ public void setInvoiceId(final UUID invoiceId) {
+ this.invoiceId = invoiceId;
+ }
+
+ public void setAccountId(final UUID accountId) {
+ this.accountId = accountId;
+ }
+
+ public void setBundleId(final UUID bundleId) {
+ this.bundleId = bundleId;
+ }
+
+ public void setSubscriptionId(final UUID subscriptionId) {
+ this.subscriptionId = subscriptionId;
+ }
+
+ public void setPlanName(final String planName) {
+ this.planName = planName;
+ }
+
+ public void setPhaseName(final String phaseName) {
+ this.phaseName = phaseName;
+ }
+
+ public void setStartDate(final LocalDate startDate) {
+ this.startDate = startDate;
+ }
+
+ public void setEndDate(final LocalDate endDate) {
+ this.endDate = endDate;
+ }
+
+ public void setAmount(final BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public void setRate(final BigDecimal rate) {
+ this.rate = rate;
+ }
+
+ public void setCurrency(final Currency currency) {
+ this.currency = currency;
+ }
+
+ public void setLinkedItemId(final UUID linkedItemId) {
+ this.linkedItemId = linkedItemId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("InvoiceItemModelDao");
+ sb.append("{type=").append(type);
+ sb.append(", invoiceId=").append(invoiceId);
+ sb.append(", accountId=").append(accountId);
+ sb.append(", bundleId=").append(bundleId);
+ sb.append(", subscriptionId=").append(subscriptionId);
+ sb.append(", planName='").append(planName).append('\'');
+ sb.append(", phaseName='").append(phaseName).append('\'');
+ sb.append(", startDate=").append(startDate);
+ sb.append(", endDate=").append(endDate);
+ sb.append(", amount=").append(amount);
+ sb.append(", rate=").append(rate);
+ sb.append(", currency=").append(currency);
+ sb.append(", linkedItemId=").append(linkedItemId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final InvoiceItemModelDao that = (InvoiceItemModelDao) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (amount != null ? amount.compareTo(that.amount) != 0 : that.amount != null) {
+ return false;
+ }
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (endDate != null ? !endDate.equals(that.endDate) : that.endDate != null) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (linkedItemId != null ? !linkedItemId.equals(that.linkedItemId) : that.linkedItemId != null) {
+ return false;
+ }
+ if (phaseName != null ? !phaseName.equals(that.phaseName) : that.phaseName != null) {
+ return false;
+ }
+ if (planName != null ? !planName.equals(that.planName) : that.planName != null) {
+ return false;
+ }
+ if (rate != null ? rate.compareTo(that.rate) != 0 : that.rate != null) {
+ return false;
+ }
+ if (startDate != null ? !startDate.equals(that.startDate) : that.startDate != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+ if (type != that.type) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ result = 31 * result + (invoiceId != null ? invoiceId.hashCode() : 0);
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (subscriptionId != null ? subscriptionId.hashCode() : 0);
+ result = 31 * result + (planName != null ? planName.hashCode() : 0);
+ result = 31 * result + (phaseName != null ? phaseName.hashCode() : 0);
+ result = 31 * result + (startDate != null ? startDate.hashCode() : 0);
+ result = 31 * result + (endDate != null ? endDate.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (rate != null ? rate.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (linkedItemId != null ? linkedItemId.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.INVOICE_ITEMS;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return null;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java
new file mode 100644
index 0000000..e4245c4
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface InvoiceItemSqlDao extends EntitySqlDao<InvoiceItemModelDao, InvoiceItem> {
+
+ @SqlQuery
+ List<InvoiceItemModelDao> getInvoiceItemsByInvoice(@Bind("invoiceId") final String invoiceId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ List<InvoiceItemModelDao> getInvoiceItemsBySubscription(@Bind("subscriptionId") final String subscriptionId,
+ @BindBean final InternalTenantContext context);
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
new file mode 100644
index 0000000..e8cef2b
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class InvoiceModelDao extends EntityBase implements EntityModelDao<Invoice> {
+
+ private UUID accountId;
+ private Integer invoiceNumber;
+ private LocalDate invoiceDate;
+ private LocalDate targetDate;
+ private Currency currency;
+ private boolean migrated;
+
+ // Note in the database, for convenience only
+ private List<InvoiceItemModelDao> invoiceItems = new LinkedList<InvoiceItemModelDao>();
+ private List<InvoicePaymentModelDao> invoicePayments = new LinkedList<InvoicePaymentModelDao>();
+ private Currency processedCurrency;
+
+ public InvoiceModelDao() { /* For the DAO mapper */ }
+
+ public InvoiceModelDao(final UUID id, @Nullable final DateTime createdDate, final UUID accountId,
+ @Nullable final Integer invoiceNumber, final LocalDate invoiceDate, final LocalDate targetDate,
+ final Currency currency, final boolean migrated) {
+ super(id, createdDate, createdDate);
+ this.accountId = accountId;
+ this.invoiceNumber = invoiceNumber;
+ this.invoiceDate = invoiceDate;
+ this.targetDate = targetDate;
+ this.currency = currency;
+ this.migrated = migrated;
+ }
+
+ public InvoiceModelDao(final UUID accountId, final LocalDate invoiceDate, final LocalDate targetDate, final Currency currency, final boolean migrated) {
+ this(UUID.randomUUID(), null, accountId, null, invoiceDate, targetDate, currency, migrated);
+ }
+
+ public InvoiceModelDao(final UUID accountId, final LocalDate invoiceDate, final LocalDate targetDate, final Currency currency) {
+ this(UUID.randomUUID(), null, accountId, null, invoiceDate, targetDate, currency, false);
+ }
+
+ public InvoiceModelDao(final Invoice invoice) {
+ this(invoice.getId(), invoice.getCreatedDate(), invoice.getAccountId(), invoice.getInvoiceNumber(), invoice.getInvoiceDate(),
+ invoice.getTargetDate(), invoice.getCurrency(), invoice.isMigrationInvoice());
+ }
+
+ public void addInvoiceItems(final List<InvoiceItemModelDao> invoiceItems) {
+ this.invoiceItems.addAll(invoiceItems);
+ }
+
+ public List<InvoiceItemModelDao> getInvoiceItems() {
+ return invoiceItems;
+ }
+
+ public void addPayments(final List<InvoicePaymentModelDao> invoicePayments) {
+ this.invoicePayments.addAll(invoicePayments);
+ }
+
+ public List<InvoicePaymentModelDao> getInvoicePayments() {
+ return invoicePayments;
+ }
+
+ public void setProcessedCurrency(Currency currency) {
+ this.processedCurrency = currency;
+ }
+
+ public Currency getProcessedCurrency() {
+ return processedCurrency != null ? processedCurrency : currency;
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public Integer getInvoiceNumber() {
+ return invoiceNumber;
+ }
+
+ public LocalDate getInvoiceDate() {
+ return invoiceDate;
+ }
+
+ public LocalDate getTargetDate() {
+ return targetDate;
+ }
+
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ public boolean isMigrated() {
+ return migrated;
+ }
+
+ public void setAccountId(final UUID accountId) {
+ this.accountId = accountId;
+ }
+
+ public void setInvoiceNumber(final Integer invoiceNumber) {
+ this.invoiceNumber = invoiceNumber;
+ }
+
+ public void setInvoiceDate(final LocalDate invoiceDate) {
+ this.invoiceDate = invoiceDate;
+ }
+
+ public void setTargetDate(final LocalDate targetDate) {
+ this.targetDate = targetDate;
+ }
+
+ public void setCurrency(final Currency currency) {
+ this.currency = currency;
+ }
+
+ public void setMigrated(final boolean migrated) {
+ this.migrated = migrated;
+ }
+
+ public void setInvoiceItems(final List<InvoiceItemModelDao> invoiceItems) {
+ this.invoiceItems = invoiceItems;
+ }
+
+ public void setInvoicePayments(final List<InvoicePaymentModelDao> invoicePayments) {
+ this.invoicePayments = invoicePayments;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("InvoiceModelDao");
+ sb.append("{accountId=").append(accountId);
+ sb.append(", invoiceNumber=").append(invoiceNumber);
+ sb.append(", invoiceDate=").append(invoiceDate);
+ sb.append(", targetDate=").append(targetDate);
+ sb.append(", currency=").append(currency);
+ sb.append(", migrated=").append(migrated);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final InvoiceModelDao that = (InvoiceModelDao) o;
+
+ if (migrated != that.migrated) {
+ return false;
+ }
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (invoiceDate != null ? !invoiceDate.equals(that.invoiceDate) : that.invoiceDate != null) {
+ return false;
+ }
+ if (invoiceNumber != null ? !invoiceNumber.equals(that.invoiceNumber) : that.invoiceNumber != null) {
+ return false;
+ }
+ if (targetDate != null ? !targetDate.equals(that.targetDate) : that.targetDate != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (invoiceNumber != null ? invoiceNumber.hashCode() : 0);
+ result = 31 * result + (invoiceDate != null ? invoiceDate.hashCode() : 0);
+ result = 31 * result + (targetDate != null ? targetDate.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (migrated ? 1 : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.INVOICES;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return null;
+ }
+
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDaoHelper.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDaoHelper.java
new file mode 100644
index 0000000..ebb19d2
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDaoHelper.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.math.BigDecimal;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.calculator.InvoiceCalculatorUtils;
+import org.killbill.billing.invoice.model.DefaultInvoicePayment;
+import org.killbill.billing.invoice.model.InvoiceItemFactory;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+
+public class InvoiceModelDaoHelper {
+
+ private InvoiceModelDaoHelper() {}
+
+ public static BigDecimal getBalance(final InvoiceModelDao invoiceModelDao) {
+ return InvoiceCalculatorUtils.computeInvoiceBalance(invoiceModelDao.getCurrency(),
+ Iterables.transform(invoiceModelDao.getInvoiceItems(), new Function<InvoiceItemModelDao, InvoiceItem>() {
+ @Override
+ public InvoiceItem apply(final InvoiceItemModelDao input) {
+ return InvoiceItemFactory.fromModelDao(input);
+ }
+ }),
+ Iterables.transform(invoiceModelDao.getInvoicePayments(), new Function<InvoicePaymentModelDao, InvoicePayment>() {
+ @Nullable
+ @Override
+ public InvoicePayment apply(final InvoicePaymentModelDao input) {
+ return new DefaultInvoicePayment(input);
+ }
+ }));
+ }
+
+ public static BigDecimal getCBAAmount(final InvoiceModelDao invoiceModelDao) {
+ return InvoiceCalculatorUtils.computeInvoiceAmountCredited(invoiceModelDao.getCurrency(),
+ Iterables.transform(invoiceModelDao.getInvoiceItems(), new Function<InvoiceItemModelDao, InvoiceItem>() {
+ @Override
+ public InvoiceItem apply(final InvoiceItemModelDao input) {
+ return InvoiceItemFactory.fromModelDao(input);
+ }
+ }));
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentModelDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentModelDao.java
new file mode 100644
index 0000000..231425c
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentModelDao.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class InvoicePaymentModelDao extends EntityBase implements EntityModelDao<InvoicePayment> {
+
+ private InvoicePaymentType type;
+ private UUID invoiceId;
+ private UUID paymentId;
+ private DateTime paymentDate;
+ private BigDecimal amount;
+ private Currency currency;
+ private Currency processedCurrency;
+ private UUID paymentCookieId;
+ private UUID linkedInvoicePaymentId;
+
+ public InvoicePaymentModelDao() { /* For the DAO mapper */ }
+
+ public InvoicePaymentModelDao(final UUID id, final DateTime createdDate, final InvoicePaymentType type, final UUID invoiceId,
+ final UUID paymentId, final DateTime paymentDate, final BigDecimal amount, final Currency currency,
+ final Currency processedCurrency, final UUID paymentCookieId, final UUID linkedInvoicePaymentId) {
+ super(id, createdDate, createdDate);
+ this.type = type;
+ this.invoiceId = invoiceId;
+ this.paymentId = paymentId;
+ this.paymentDate = paymentDate;
+ this.amount = amount;
+ this.currency = currency;
+ this.processedCurrency = processedCurrency;
+ this.paymentCookieId = paymentCookieId;
+ this.linkedInvoicePaymentId = linkedInvoicePaymentId;
+ }
+
+ public InvoicePaymentModelDao(final InvoicePayment invoicePayment) {
+ this(invoicePayment.getId(), invoicePayment.getCreatedDate(), invoicePayment.getType(), invoicePayment.getInvoiceId(), invoicePayment.getPaymentId(),
+ invoicePayment.getPaymentDate(), invoicePayment.getAmount(), invoicePayment.getCurrency(), invoicePayment.getProcessedCurrency(), invoicePayment.getPaymentCookieId(),
+ invoicePayment.getLinkedInvoicePaymentId());
+ }
+
+ public InvoicePaymentType getType() {
+ return type;
+ }
+
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ public UUID getPaymentId() {
+ return paymentId;
+ }
+
+ public DateTime getPaymentDate() {
+ return paymentDate;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ public Currency getProcessedCurrency() {
+ return processedCurrency;
+ }
+
+ public UUID getPaymentCookieId() {
+ return paymentCookieId;
+ }
+
+ public UUID getLinkedInvoicePaymentId() {
+ return linkedInvoicePaymentId;
+ }
+
+ public void setType(final InvoicePaymentType type) {
+ this.type = type;
+ }
+
+ public void setInvoiceId(final UUID invoiceId) {
+ this.invoiceId = invoiceId;
+ }
+
+ public void setPaymentId(final UUID paymentId) {
+ this.paymentId = paymentId;
+ }
+
+ public void setPaymentDate(final DateTime paymentDate) {
+ this.paymentDate = paymentDate;
+ }
+
+ public void setAmount(final BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public void setCurrency(final Currency currency) {
+ this.currency = currency;
+ }
+
+ public void setProcessedCurrency(final Currency processedCurrency) {
+ this.processedCurrency = processedCurrency;
+ }
+
+ public void setPaymentCookieId(final UUID paymentCookieId) {
+ this.paymentCookieId = paymentCookieId;
+ }
+
+ public void setLinkedInvoicePaymentId(final UUID linkedInvoicePaymentId) {
+ this.linkedInvoicePaymentId = linkedInvoicePaymentId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("InvoicePaymentModelDao");
+ sb.append("{type=").append(type);
+ sb.append(", invoiceId=").append(invoiceId);
+ sb.append(", paymentId=").append(paymentId);
+ sb.append(", paymentDate=").append(paymentDate);
+ sb.append(", amount=").append(amount);
+ sb.append(", currency=").append(currency);
+ sb.append(", paymentCookieId=").append(paymentCookieId);
+ sb.append(", linkedInvoicePaymentId=").append(linkedInvoicePaymentId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final InvoicePaymentModelDao that = (InvoicePaymentModelDao) o;
+
+ if (amount != null ? !amount.equals(that.amount) : that.amount != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (linkedInvoicePaymentId != null ? !linkedInvoicePaymentId.equals(that.linkedInvoicePaymentId) : that.linkedInvoicePaymentId != null) {
+ return false;
+ }
+ if (paymentCookieId != null ? !paymentCookieId.equals(that.paymentCookieId) : that.paymentCookieId != null) {
+ return false;
+ }
+ if (paymentDate != null ? !paymentDate.equals(that.paymentDate) : that.paymentDate != null) {
+ return false;
+ }
+ if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
+ return false;
+ }
+ if (type != that.type) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ result = 31 * result + (invoiceId != null ? invoiceId.hashCode() : 0);
+ result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
+ result = 31 * result + (paymentDate != null ? paymentDate.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (paymentCookieId != null ? paymentCookieId.hashCode() : 0);
+ result = 31 * result + (linkedInvoicePaymentId != null ? linkedInvoicePaymentId.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.INVOICE_PAYMENTS;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return null;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
new file mode 100644
index 0000000..474b51c
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlBatch;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
+
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.dao.UuidMapper;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface InvoicePaymentSqlDao extends EntitySqlDao<InvoicePaymentModelDao, InvoicePayment> {
+
+ @SqlQuery
+ public InvoicePaymentModelDao getByPaymentId(@Bind("paymentId") final String paymentId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlBatch(transactional = false)
+ @Audited(ChangeType.INSERT)
+ void batchCreateFromTransaction(@BindBean final List<InvoicePaymentModelDao> items,
+ @BindBean final InternalCallContext context);
+
+ @SqlQuery
+ public List<InvoicePaymentModelDao> getPaymentsForInvoice(@Bind("invoiceId") final String invoiceId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ List<InvoicePaymentModelDao> getInvoicePayments(@Bind("paymentId") final String paymentId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ InvoicePaymentModelDao getPaymentsForCookieId(@Bind("paymentCookieId") final String paymentCookieId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ BigDecimal getRemainingAmountPaid(@Bind("invoicePaymentId") final String invoicePaymentId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @RegisterMapper(UuidMapper.class)
+ UUID getAccountIdFromInvoicePaymentId(@Bind("invoicePaymentId") final String invoicePaymentId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ List<InvoicePaymentModelDao> getChargeBacksByAccountId(@Bind("accountId") final String accountId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ List<InvoicePaymentModelDao> getChargebacksByPaymentId(@Bind("paymentId") final String paymentId,
+ @BindBean final InternalTenantContext context);
+}
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
new file mode 100644
index 0000000..40551e9
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceSqlDao.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface InvoiceSqlDao extends EntitySqlDao<InvoiceModelDao, Invoice> {
+
+ @SqlQuery
+ List<InvoiceModelDao> getInvoicesBySubscription(@Bind("subscriptionId") final String subscriptionId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ UUID getInvoiceIdByPaymentId(@Bind("paymentId") final String paymentId,
+ @BindBean final InternalTenantContext context);
+}
+
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
new file mode 100644
index 0000000..6809742
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.generator;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+
+import com.google.common.annotations.VisibleForTesting;
+
+public class BillingIntervalDetail {
+
+ private final LocalDate startDate;
+ private final LocalDate endDate;
+ private final LocalDate targetDate;
+ private final int billingCycleDay;
+ private final BillingPeriod billingPeriod;
+
+ private LocalDate firstBillingCycleDate;
+ private LocalDate effectiveEndDate;
+ private LocalDate lastBillingCycleDate;
+
+ public BillingIntervalDetail(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate, final int billingCycleDay, final BillingPeriod billingPeriod) {
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.targetDate = targetDate;
+ this.billingCycleDay = billingCycleDay;
+ this.billingPeriod = billingPeriod;
+ computeAll();
+ }
+
+ public LocalDate getFirstBillingCycleDate() {
+ return firstBillingCycleDate;
+ }
+
+ public LocalDate getEffectiveEndDate() {
+ return effectiveEndDate;
+ }
+
+ public LocalDate getFutureBillingDateFor(int nbPeriod) {
+ final int numberOfMonthsPerBillingPeriod = billingPeriod.getNumberOfMonths();
+ LocalDate proposedDate = firstBillingCycleDate.plusMonths((nbPeriod) * numberOfMonthsPerBillingPeriod);
+ return alignProposedBillCycleDate(proposedDate);
+ }
+
+ public LocalDate getLastBillingCycleDate() {
+ return lastBillingCycleDate;
+ }
+
+ private void computeAll() {
+ calculateFirstBillingCycleDate();
+ calculateEffectiveEndDate();
+ calculateLastBillingCycleDate();
+ }
+
+ @VisibleForTesting
+ void calculateFirstBillingCycleDate() {
+
+ final int lastDayOfMonth = startDate.dayOfMonth().getMaximumValue();
+ final LocalDate billingCycleDate;
+ if (billingCycleDay > lastDayOfMonth) {
+ billingCycleDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), lastDayOfMonth, startDate.getChronology());
+ } else {
+ billingCycleDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), billingCycleDay, startDate.getChronology());
+ }
+
+ final int numberOfMonthsInPeriod = billingPeriod.getNumberOfMonths();
+ LocalDate proposedDate = billingCycleDate;
+ while (proposedDate.isBefore(startDate)) {
+ proposedDate = proposedDate.plusMonths(numberOfMonthsInPeriod);
+ }
+ firstBillingCycleDate = alignProposedBillCycleDate(proposedDate);
+ }
+
+ private void calculateEffectiveEndDate() {
+
+ // We have an endDate and the targetDate is greater or equal to our endDate => return it
+ if (endDate != null && !targetDate.isBefore(endDate)) {
+ effectiveEndDate = endDate;
+ return;
+ }
+
+ if (targetDate.isBefore(firstBillingCycleDate)) {
+ effectiveEndDate = firstBillingCycleDate;
+ return;
+ }
+
+ final int numberOfMonthsInPeriod = billingPeriod.getNumberOfMonths();
+ int numberOfPeriods = 0;
+ LocalDate proposedDate = firstBillingCycleDate;
+
+ while (!proposedDate.isAfter(targetDate)) {
+ proposedDate = firstBillingCycleDate.plusMonths(numberOfPeriods * numberOfMonthsInPeriod);
+ numberOfPeriods += 1;
+ }
+ proposedDate = alignProposedBillCycleDate(proposedDate);
+
+ // The proposedDate is greater to our endDate => return it
+ if (endDate != null && endDate.isBefore(proposedDate)) {
+ effectiveEndDate = endDate;
+ } else {
+ effectiveEndDate = proposedDate;
+ }
+ }
+
+
+ private void calculateLastBillingCycleDate() {
+
+ // Start from firstBillingCycleDate and billingPeriod until we pass the effectiveEndDate
+ LocalDate proposedDate = firstBillingCycleDate;
+ int numberOfPeriods = 0;
+ while (!proposedDate.isAfter(effectiveEndDate)) {
+ proposedDate = firstBillingCycleDate.plusMonths(numberOfPeriods * billingPeriod.getNumberOfMonths());
+ numberOfPeriods += 1;
+ }
+
+ // Our proposed date is billingCycleDate prior to the effectiveEndDate
+ proposedDate = proposedDate.plusMonths(-billingPeriod.getNumberOfMonths());
+ proposedDate = alignProposedBillCycleDate(proposedDate);
+
+ if (proposedDate.isBefore(firstBillingCycleDate)) {
+ // Make sure not to go too far in the past
+ lastBillingCycleDate = firstBillingCycleDate;
+ } else {
+ lastBillingCycleDate = proposedDate;
+ }
+ }
+
+
+ //
+ // We start from a billCycleDate
+ //
+ private LocalDate alignProposedBillCycleDate(final LocalDate proposedDate) {
+ final int lastDayOfMonth = proposedDate.dayOfMonth().getMaximumValue();
+
+ int proposedBillCycleDate = proposedDate.getDayOfMonth();
+ if (proposedBillCycleDate < billingCycleDay && billingCycleDay <= lastDayOfMonth) {
+ proposedBillCycleDate = billingCycleDay;
+ }
+ return new LocalDate(proposedDate.getYear(), proposedDate.getMonthOfYear(), proposedBillCycleDate, proposedDate.getChronology());
+ }
+}
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
new file mode 100644
index 0000000..10d8bc8
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.generator;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.joda.time.Months;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.clock.Clock;
+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.model.BillingMode;
+import org.killbill.billing.invoice.model.DefaultInvoice;
+import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.invoice.model.InAdvanceBillingMode;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
+import org.killbill.billing.invoice.tree.AccountItemTree;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+import org.killbill.billing.junction.BillingModeType;
+import org.killbill.billing.util.config.InvoiceConfig;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+import com.google.inject.Inject;
+
+public class DefaultInvoiceGenerator implements InvoiceGenerator {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceGenerator.class);
+
+ private final Clock clock;
+ private final InvoiceConfig config;
+
+ @Inject
+ public DefaultInvoiceGenerator(final Clock clock, final InvoiceConfig config) {
+ this.clock = clock;
+ this.config = config;
+ }
+
+ /*
+ * adjusts target date to the maximum invoice target date, if future invoices exist
+ */
+ @Override
+ public Invoice generateInvoice(final UUID accountId, @Nullable final BillingEventSet events,
+ @Nullable final List<Invoice> existingInvoices,
+ final LocalDate targetDate,
+ final Currency targetCurrency) throws InvoiceApiException {
+ if ((events == null) || (events.size() == 0) || events.isAccountAutoInvoiceOff()) {
+ return null;
+ }
+
+ validateTargetDate(targetDate);
+ final LocalDate adjustedTargetDate = adjustTargetDate(existingInvoices, targetDate);
+
+ final Invoice invoice = new DefaultInvoice(accountId, clock.getUTCToday(), adjustedTargetDate, targetCurrency);
+ final UUID invoiceId = invoice.getId();
+
+ final List<InvoiceItem> inAdvanceItems = generateInAdvanceInvoiceItems(accountId, invoiceId, events, existingInvoices, adjustedTargetDate, targetCurrency);
+ invoice.addInvoiceItems(inAdvanceItems);
+
+ return inAdvanceItems.size() != 0 ? invoice : null;
+ }
+
+ private List<InvoiceItem> generateInAdvanceInvoiceItems(final UUID accountId, final UUID invoiceId, @Nullable final BillingEventSet events,
+ @Nullable final List<Invoice> existingInvoices, final LocalDate targetDate,
+ final Currency targetCurrency) throws InvoiceApiException {
+ final AccountItemTree accountItemTree = new AccountItemTree(accountId);
+ if (existingInvoices != null) {
+ for (final Invoice invoice : existingInvoices) {
+ for (final InvoiceItem item : invoice.getInvoiceItems()) {
+ if (item.getSubscriptionId() == null || // Always include migration invoices, credits, external charges etc.
+ !events.getSubscriptionIdsWithAutoInvoiceOff()
+ .contains(item.getSubscriptionId())) { //don't add items with auto_invoice_off tag
+ accountItemTree.addExistingItem(item);
+ }
+ }
+ }
+ }
+
+ // Generate list of proposed invoice items based on billing events from junction-- proposed items are ALL items since beginning of time
+ final List<InvoiceItem> proposedItems = generateInAdvanceInvoiceItems(invoiceId, accountId, events, targetDate, targetCurrency);
+
+ accountItemTree.mergeWithProposedItems(proposedItems);
+ return accountItemTree.getResultingItemList();
+ }
+
+ private void validateTargetDate(final LocalDate targetDate) throws InvoiceApiException {
+ final int maximumNumberOfMonths = config.getNumberOfMonthsInFuture();
+
+ if (Months.monthsBetween(clock.getUTCToday(), targetDate).getMonths() > maximumNumberOfMonths) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_TARGET_DATE_TOO_FAR_IN_THE_FUTURE, targetDate.toString());
+ }
+ }
+
+ private LocalDate adjustTargetDate(final List<Invoice> existingInvoices, final LocalDate targetDate) {
+ if (existingInvoices == null) {
+ return targetDate;
+ }
+
+ LocalDate maxDate = targetDate;
+
+ for (final Invoice invoice : existingInvoices) {
+ if (invoice.getTargetDate().isAfter(maxDate)) {
+ maxDate = invoice.getTargetDate();
+ }
+ }
+ return maxDate;
+ }
+
+ private List<InvoiceItem> generateInAdvanceInvoiceItems(final UUID invoiceId, final UUID accountId, final BillingEventSet events,
+ final LocalDate targetDate, final Currency currency) throws InvoiceApiException {
+ final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
+
+ if (events.size() == 0) {
+ return items;
+ }
+
+ // Pretty-print the generated invoice items from the junction events
+ final StringBuilder logStringBuilder = new StringBuilder("Invoice items generated for invoiceId ")
+ .append(invoiceId)
+ .append(" and accountId ")
+ .append(accountId);
+
+ final Iterator<BillingEvent> eventIt = events.iterator();
+ BillingEvent nextEvent = eventIt.next();
+ while (eventIt.hasNext()) {
+ final BillingEvent thisEvent = nextEvent;
+ nextEvent = eventIt.next();
+ if (!events.getSubscriptionIdsWithAutoInvoiceOff().
+ contains(thisEvent.getSubscription().getId())) { // don't consider events for subscriptions that have auto_invoice_off
+ final BillingEvent adjustedNextEvent = (thisEvent.getSubscription().getId() == nextEvent.getSubscription().getId()) ? nextEvent : null;
+ items.addAll(processInAdvanceEvents(invoiceId, accountId, thisEvent, adjustedNextEvent, targetDate, currency, logStringBuilder));
+ }
+ }
+ items.addAll(processInAdvanceEvents(invoiceId, accountId, nextEvent, null, targetDate, currency, logStringBuilder));
+
+ log.info(logStringBuilder.toString());
+
+ return items;
+ }
+
+ // Turn a set of events into a list of invoice items. Note that the dates on the invoice items will be rounded (granularity of a day)
+ private List<InvoiceItem> processInAdvanceEvents(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent, @Nullable final BillingEvent nextEvent,
+ final LocalDate targetDate, final Currency currency,
+ final StringBuilder logStringBuilder) throws InvoiceApiException {
+ final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
+
+ // Handle fixed price items
+ final InvoiceItem fixedPriceInvoiceItem = generateFixedPriceItem(invoiceId, accountId, thisEvent, targetDate, currency);
+ if (fixedPriceInvoiceItem != null) {
+ items.add(fixedPriceInvoiceItem);
+ }
+
+ // Handle recurring items
+ final BillingPeriod billingPeriod = thisEvent.getBillingPeriod();
+ if (billingPeriod != BillingPeriod.NO_BILLING_PERIOD) {
+ final BillingMode billingMode = instantiateBillingMode(thisEvent.getBillingMode());
+ final LocalDate startDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone());
+
+ if (!startDate.isAfter(targetDate)) {
+ final LocalDate endDate = (nextEvent == null) ? null : new LocalDate(nextEvent.getEffectiveDate(), nextEvent.getTimeZone());
+
+ final int billCycleDayLocal = thisEvent.getBillCycleDayLocal();
+
+ final List<RecurringInvoiceItemData> itemData;
+ try {
+ itemData = billingMode.calculateInvoiceItemData(startDate, endDate, targetDate, billCycleDayLocal, billingPeriod);
+ } catch (InvalidDateSequenceException e) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_INVALID_DATE_SEQUENCE, startDate, endDate, targetDate);
+ }
+
+ for (final RecurringInvoiceItemData itemDatum : itemData) {
+ final BigDecimal rate = thisEvent.getRecurringPrice();
+
+ if (rate != null) {
+ final BigDecimal amount = KillBillMoney.of(itemDatum.getNumberOfCycles().multiply(rate), currency);
+
+ final RecurringInvoiceItem recurringItem = new RecurringInvoiceItem(invoiceId,
+ accountId,
+ thisEvent.getSubscription().getBundleId(),
+ thisEvent.getSubscription().getId(),
+ thisEvent.getPlan().getName(),
+ thisEvent.getPlanPhase().getName(),
+ itemDatum.getStartDate(), itemDatum.getEndDate(),
+ amount, rate, currency);
+ items.add(recurringItem);
+ }
+ }
+ }
+ }
+
+ // For debugging purposes
+ logStringBuilder.append("\n")
+ .append(thisEvent);
+ for (final InvoiceItem item : items) {
+ logStringBuilder.append("\n\t")
+ .append(item);
+ }
+
+ return items;
+ }
+
+ private BillingMode instantiateBillingMode(final BillingModeType billingMode) {
+ switch (billingMode) {
+ case IN_ADVANCE:
+ return new InAdvanceBillingMode();
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ InvoiceItem generateFixedPriceItem(final UUID invoiceId, final UUID accountId, final BillingEvent thisEvent,
+ final LocalDate targetDate, final Currency currency) {
+ final LocalDate roundedStartDate = new LocalDate(thisEvent.getEffectiveDate(), thisEvent.getTimeZone());
+
+ if (roundedStartDate.isAfter(targetDate)) {
+ return null;
+ } else {
+ final BigDecimal fixedPrice = thisEvent.getFixedPrice();
+
+ if (fixedPrice != null) {
+ return new FixedPriceInvoiceItem(invoiceId, accountId, thisEvent.getSubscription().getBundleId(),
+ thisEvent.getSubscription().getId(),
+ thisEvent.getPlan().getName(), thisEvent.getPlanPhase().getName(),
+ roundedStartDate, fixedPrice, currency);
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceDateUtils.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceDateUtils.java
new file mode 100644
index 0000000..f4f4a38
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceDateUtils.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.generator;
+
+import java.math.BigDecimal;
+
+import org.joda.time.Days;
+import org.joda.time.LocalDate;
+import org.joda.time.Months;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public class InvoiceDateUtils {
+
+ /**
+ * Called internally to calculate proration or when we recalculate approximate repair amount
+ *
+ * @param startDate start date of the prorated interval
+ * @param endDate end date of the prorated interval
+ * @param previousBillingCycleDate start date of the period
+ * @param nextBillingCycleDate end date of the period
+ * @return
+ */
+ public static BigDecimal calculateProrationBetweenDates(final LocalDate startDate, final LocalDate endDate, final LocalDate previousBillingCycleDate, final LocalDate nextBillingCycleDate) {
+ final int daysBetween = Days.daysBetween(previousBillingCycleDate, nextBillingCycleDate).getDays();
+ return calculateProrationBetweenDates(startDate, endDate, daysBetween);
+ }
+
+ public static BigDecimal calculateProrationBetweenDates(final LocalDate startDate, final LocalDate endDate, int daysBetween) {
+ if (daysBetween <= 0) {
+ return BigDecimal.ZERO;
+ }
+
+ final BigDecimal daysInPeriod = new BigDecimal(daysBetween);
+ final BigDecimal days = new BigDecimal(Days.daysBetween(startDate, endDate).getDays());
+
+ return days.divide(daysInPeriod, KillBillMoney.MAX_SCALE, KillBillMoney.ROUNDING_METHOD);
+ }
+
+ public static BigDecimal calculateProRationBeforeFirstBillingPeriod(final LocalDate startDate, final LocalDate nextBillingCycleDate,
+ final BillingPeriod billingPeriod) {
+ final LocalDate previousBillingCycleDate = nextBillingCycleDate.plusMonths(-billingPeriod.getNumberOfMonths());
+
+ return calculateProrationBetweenDates(startDate, nextBillingCycleDate, previousBillingCycleDate, nextBillingCycleDate);
+ }
+
+ public static int calculateNumberOfWholeBillingPeriods(final LocalDate startDate, final LocalDate endDate, final BillingPeriod billingPeriod) {
+ final int numberOfMonths = Months.monthsBetween(startDate, endDate).getMonths();
+ final int numberOfMonthsInPeriod = billingPeriod.getNumberOfMonths();
+ return numberOfMonths / numberOfMonthsInPeriod;
+ }
+
+ public static LocalDate calculateLastBillingCycleDateBefore(final LocalDate date, final LocalDate previousBillCycleDate,
+ final int billingCycleDay, final BillingPeriod billingPeriod) {
+ LocalDate proposedDate = previousBillCycleDate;
+
+ int numberOfPeriods = 0;
+ while (!proposedDate.isAfter(date)) {
+ proposedDate = previousBillCycleDate.plusMonths(numberOfPeriods * billingPeriod.getNumberOfMonths());
+ numberOfPeriods += 1;
+ }
+
+ proposedDate = proposedDate.plusMonths(-billingPeriod.getNumberOfMonths());
+
+ if (proposedDate.dayOfMonth().get() < billingCycleDay) {
+ final int lastDayOfTheMonth = proposedDate.dayOfMonth().getMaximumValue();
+ if (lastDayOfTheMonth < billingCycleDay) {
+ proposedDate = new LocalDate(proposedDate.getYear(), proposedDate.getMonthOfYear(), lastDayOfTheMonth);
+ } else {
+ proposedDate = new LocalDate(proposedDate.getYear(), proposedDate.getMonthOfYear(), billingCycleDay);
+ }
+ }
+
+ if (proposedDate.isBefore(previousBillCycleDate)) {
+ // Make sure not to go too far in the past
+ return previousBillCycleDate;
+ } else {
+ return proposedDate;
+ }
+ }
+
+ public static LocalDate calculateEffectiveEndDate(final LocalDate billCycleDate, final LocalDate targetDate,
+ final BillingPeriod billingPeriod) {
+ if (targetDate.isBefore(billCycleDate)) {
+ return billCycleDate;
+ }
+
+ final int numberOfMonthsInPeriod = billingPeriod.getNumberOfMonths();
+ int numberOfPeriods = 0;
+ LocalDate proposedDate = billCycleDate;
+
+ while (!proposedDate.isAfter(targetDate)) {
+ proposedDate = billCycleDate.plusMonths(numberOfPeriods * numberOfMonthsInPeriod);
+ numberOfPeriods += 1;
+ }
+
+ return proposedDate;
+ }
+
+ public static LocalDate calculateEffectiveEndDate(final LocalDate billCycleDate, final LocalDate targetDate,
+ final LocalDate endDate, final BillingPeriod billingPeriod) {
+ if (targetDate.isBefore(endDate)) {
+ if (targetDate.isBefore(billCycleDate)) {
+ return billCycleDate;
+ }
+
+ final int numberOfMonthsInPeriod = billingPeriod.getNumberOfMonths();
+ int numberOfPeriods = 0;
+ LocalDate proposedDate = billCycleDate;
+
+ while (!proposedDate.isAfter(targetDate)) {
+ proposedDate = billCycleDate.plusMonths(numberOfPeriods * numberOfMonthsInPeriod);
+ numberOfPeriods += 1;
+ }
+
+ // the current period includes the target date
+ // check to see whether the end date truncates the period
+ if (endDate.isBefore(proposedDate)) {
+ return endDate;
+ } else {
+ return proposedDate;
+ }
+ } else {
+ return endDate;
+ }
+ }
+
+ public static BigDecimal calculateProRationAfterLastBillingCycleDate(final LocalDate endDate, final LocalDate previousBillThroughDate,
+ final BillingPeriod billingPeriod) {
+ // Note: assumption is that previousBillThroughDate is correctly aligned with the billing cycle day
+ final LocalDate nextBillThroughDate = previousBillThroughDate.plusMonths(billingPeriod.getNumberOfMonths());
+ return calculateProrationBetweenDates(previousBillThroughDate, endDate, previousBillThroughDate, nextBillThroughDate);
+ }
+
+ /*
+ public static LocalDate calculateBillingCycleDateOnOrAfter(final LocalDate date, final DateTimeZone accountTimeZone,
+ final int billingCycleDayLocal) {
+ final DateTime tmp = date.toDateTimeAtStartOfDay(accountTimeZone);
+ final DateTime proposedDateTime = calculateBillingCycleDateOnOrAfter(tmp, billingCycleDayLocal);
+
+ return new LocalDate(proposedDateTime, accountTimeZone);
+ }
+
+ public static LocalDate calculateBillingCycleDateAfter(final LocalDate date, final DateTimeZone accountTimeZone,
+ final int billingCycleDayLocal) {
+ final DateTime tmp = date.toDateTimeAtStartOfDay(accountTimeZone);
+ final DateTime proposedDateTime = calculateBillingCycleDateAfter(tmp, billingCycleDayLocal);
+
+ return new LocalDate(proposedDateTime, accountTimeZone);
+ }
+ */
+
+ public static LocalDate calculateBillingCycleDateOnOrAfter(final LocalDate date, final int billingCycleDayLocal) {
+ final int lastDayOfMonth = date.dayOfMonth().getMaximumValue();
+
+ final LocalDate fixedDate;
+ if (billingCycleDayLocal > lastDayOfMonth) {
+ fixedDate = new LocalDate(date.getYear(), date.getMonthOfYear(), lastDayOfMonth, date.getChronology());
+ } else {
+ fixedDate = new LocalDate(date.getYear(), date.getMonthOfYear(), billingCycleDayLocal, date.getChronology());
+ }
+
+ LocalDate proposedDate = fixedDate;
+ while (proposedDate.isBefore(date)) {
+ proposedDate = proposedDate.plusMonths(1);
+ }
+ return proposedDate;
+ }
+
+ public static LocalDate calculateBillingCycleDateAfter(final LocalDate date, final int billingCycleDayLocal) {
+ LocalDate proposedDate = calculateBillingCycleDateOnOrAfter(date, billingCycleDayLocal);
+ if (date.compareTo(proposedDate) == 0) {
+ proposedDate = proposedDate.plusMonths(1);
+ }
+ return proposedDate;
+ }
+}
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
new file mode 100644
index 0000000..014dbcd
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceGenerator.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.generator;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.junction.BillingEventSet;
+
+public interface InvoiceGenerator {
+
+ public Invoice generateInvoice(UUID accountId, @Nullable BillingEventSet events, @Nullable List<Invoice> existingInvoices,
+ LocalDate targetDate, Currency targetCurrency) 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
new file mode 100644
index 0000000..3a035b2
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.glue.InvoiceModule;
+import org.killbill.billing.invoice.InvoiceListener;
+import org.killbill.billing.invoice.InvoiceTagHandler;
+import org.killbill.billing.invoice.api.DefaultInvoiceService;
+import org.killbill.billing.invoice.api.InvoiceMigrationApi;
+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;
+import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
+import org.killbill.billing.invoice.api.invoice.DefaultInvoicePaymentApi;
+import org.killbill.billing.invoice.api.migration.DefaultInvoiceMigrationApi;
+import org.killbill.billing.invoice.api.svcs.DefaultInvoiceInternalApi;
+import org.killbill.billing.invoice.api.user.DefaultInvoiceUserApi;
+import org.killbill.billing.invoice.dao.DefaultInvoiceDao;
+import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.generator.DefaultInvoiceGenerator;
+import org.killbill.billing.invoice.generator.InvoiceGenerator;
+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.util.config.InvoiceConfig;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+import com.google.inject.AbstractModule;
+
+public class DefaultInvoiceModule extends AbstractModule implements InvoiceModule {
+
+ InvoiceConfig config;
+
+ protected final ConfigSource configSource;
+
+ public DefaultInvoiceModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ protected void installInvoiceDao() {
+ bind(InvoiceDao.class).to(DefaultInvoiceDao.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installInvoiceUserApi() {
+ bind(InvoiceUserApi.class).to(DefaultInvoiceUserApi.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installInvoiceInternalApi() {
+ bind(InvoiceInternalApi.class).to(DefaultInvoiceInternalApi.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installInvoicePaymentApi() {
+ bind(InvoicePaymentApi.class).to(DefaultInvoicePaymentApi.class).asEagerSingleton();
+ }
+
+ protected void installConfig() {
+ config = new ConfigurationObjectFactory(configSource).build(InvoiceConfig.class);
+ bind(InvoiceConfig.class).toInstance(config);
+ }
+
+ protected void installInvoiceService() {
+ bind(InvoiceService.class).to(DefaultInvoiceService.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installInvoiceMigrationApi() {
+ bind(InvoiceMigrationApi.class).to(DefaultInvoiceMigrationApi.class).asEagerSingleton();
+ }
+
+ protected void installNotifiers() {
+ bind(NextBillingDateNotifier.class).to(DefaultNextBillingDateNotifier.class).asEagerSingleton();
+ bind(NextBillingDatePoster.class).to(DefaultNextBillingDatePoster.class).asEagerSingleton();
+ final TranslatorConfig config = new ConfigurationObjectFactory(configSource).build(TranslatorConfig.class);
+ bind(TranslatorConfig.class).toInstance(config);
+ bind(InvoiceFormatterFactory.class).to(config.getInvoiceFormatterFactoryClass()).asEagerSingleton();
+ }
+
+ protected void installInvoiceNotifier() {
+ if (config.isEmailNotificationsEnabled()) {
+ bind(InvoiceNotifier.class).to(EmailInvoiceNotifier.class).asEagerSingleton();
+ } else {
+ bind(InvoiceNotifier.class).to(NullInvoiceNotifier.class).asEagerSingleton();
+ }
+ }
+
+ protected void installInvoiceListener() {
+ bind(InvoiceListener.class).asEagerSingleton();
+ }
+
+ protected void installTagHandler() {
+ bind(InvoiceTagHandler.class).asEagerSingleton();
+ }
+
+ protected void installInvoiceGenerator() {
+ bind(InvoiceGenerator.class).to(DefaultInvoiceGenerator.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ installConfig();
+
+ installInvoiceService();
+ installInvoiceNotifier();
+ installNotifiers();
+ installInvoiceListener();
+ installTagHandler();
+ installInvoiceGenerator();
+ installInvoiceDao();
+ installInvoiceUserApi();
+ installInvoiceInternalApi();
+ installInvoicePaymentApi();
+ installInvoiceMigrationApi();
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
new file mode 100644
index 0000000..ed949df
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceListener.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.events.BlockingTransitionInternalEvent;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.events.EffectiveEntitlementInternalEvent;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.events.RepairSubscriptionInternalEvent;
+import org.killbill.billing.util.config.InvoiceConfig;
+
+import com.google.common.eventbus.Subscribe;
+import com.google.inject.Inject;
+
+public class InvoiceListener {
+
+ private static final Logger log = LoggerFactory.getLogger(InvoiceListener.class);
+
+ private final InvoiceDispatcher dispatcher;
+ private final InternalCallContextFactory internalCallContextFactory;
+ private final AccountInternalApi accountApi;
+ private final InvoiceConfig invoiceConfig;
+ private final Clock clock;
+
+ @Inject
+ public InvoiceListener(final AccountInternalApi accountApi, final Clock clock, final InternalCallContextFactory internalCallContextFactory,
+ final InvoiceConfig invoiceConfig, final InvoiceDispatcher dispatcher) {
+ this.accountApi = accountApi;
+ this.dispatcher = dispatcher;
+ this.invoiceConfig = invoiceConfig;
+ this.internalCallContextFactory = internalCallContextFactory;
+ this.clock = clock;
+ }
+
+ @Subscribe
+ public void handleRepairSubscriptionEvent(final RepairSubscriptionInternalEvent event) {
+
+ try {
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "RepairBundle", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
+ dispatcher.processAccount(event.getAccountId(), event.getEffectiveDate(), false, context);
+ } catch (InvoiceApiException e) {
+ log.error(e.getMessage());
+ }
+ }
+
+ @Subscribe
+ public void handleSubscriptionTransition(final EffectiveSubscriptionInternalEvent event) {
+
+ try {
+ // Skip future uncancel event
+ // Skip events which are marked as not being the last one
+ if (event.getTransitionType() == SubscriptionBaseTransitionType.UNCANCEL ||
+ event.getTransitionType() == SubscriptionBaseTransitionType.MIGRATE_ENTITLEMENT
+ || event.getRemainingEventsForUserOperation() > 0) {
+ return;
+ }
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
+ dispatcher.processSubscription(event, context);
+ } catch (InvoiceApiException e) {
+ log.error(e.getMessage());
+ }
+ }
+
+ @Subscribe
+ public void handleEntitlementTransition(final EffectiveEntitlementInternalEvent event) {
+
+ try {
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
+ dispatcher.processAccount(event.getAccountId(), event.getEffectiveTransitionTime(), false, context);
+ } catch (InvoiceApiException e) {
+ log.error(e.getMessage());
+ }
+ }
+
+ @Subscribe
+ public void handleBlockingStateTransition(final BlockingTransitionInternalEvent event) {
+
+ // We are only interested in blockBilling or unblockBilling transitions.
+ if (!event.isTransitionedToUnblockedBilling() && !event.isTransitionedToBlockedBilling()) {
+ return;
+ }
+
+ try {
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "SubscriptionBaseTransition", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
+ final UUID accountId = accountApi.getByRecordId(event.getSearchKey1(), context);
+ dispatcher.processAccount(accountId, clock.getUTCNow(), false, context);
+ } catch (InvoiceApiException e) {
+ log.error(e.getMessage());
+ } catch (AccountApiException e) {
+ log.error(e.getMessage());
+ }
+ }
+
+ public void handleNextBillingDateEvent(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ try {
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "Next Billing Date", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
+ dispatcher.processSubscription(subscriptionId, eventDateTime, context);
+ } catch (InvoiceApiException e) {
+ log.error(e.getMessage());
+ }
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
new file mode 100644
index 0000000..7d0d5c6
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceTagHandler.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice;
+
+import java.util.UUID;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.clock.Clock;
+import org.killbill.billing.events.ControlTagDeletionInternalEvent;
+import org.killbill.billing.util.tag.ControlTagType;
+
+import com.google.common.eventbus.Subscribe;
+import com.google.inject.Inject;
+
+public class InvoiceTagHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(InvoiceTagHandler.class);
+
+ private final Clock clock;
+ private final InvoiceDispatcher dispatcher;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public InvoiceTagHandler(final Clock clock,
+ final InvoiceDispatcher dispatcher,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.clock = clock;
+ this.dispatcher = dispatcher;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Subscribe
+ public void process_AUTO_INVOICING_OFF_removal(final ControlTagDeletionInternalEvent event) {
+
+ if (event.getTagDefinition().getName().equals(ControlTagType.AUTO_INVOICING_OFF.toString()) && event.getObjectType() == ObjectType.ACCOUNT) {
+ final UUID accountId = event.getObjectId();
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "InvoiceTagHandler", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
+ processUnpaid_AUTO_INVOICING_OFF_invoices(accountId, context);
+ }
+ }
+
+ private void processUnpaid_AUTO_INVOICING_OFF_invoices(final UUID accountId, final InternalCallContext context) {
+ try {
+ dispatcher.processAccount(accountId, clock.getUTCNow(), false, context);
+ } catch (InvoiceApiException e) {
+ log.warn(String.format("Failed to process process removal AUTO_INVOICING_OFF for account %s", accountId), e);
+ }
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/AdjInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/AdjInvoiceItem.java
new file mode 100644
index 0000000..5cd34c5
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/AdjInvoiceItem.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public abstract class AdjInvoiceItem extends InvoiceItemBase {
+
+ AdjInvoiceItem(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency) {
+ this(id, createdDate, invoiceId, accountId, startDate, endDate, amount, currency, null);
+ }
+
+ AdjInvoiceItem(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency, @Nullable final UUID reversingId) {
+ super(id, createdDate, invoiceId, accountId, null, null, null, null, startDate, endDate, amount, currency, reversingId);
+ }
+
+
+ @Override
+ public abstract InvoiceItemType getInvoiceItemType();
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/BillingMode.java b/invoice/src/main/java/org/killbill/billing/invoice/model/BillingMode.java
new file mode 100644
index 0000000..91f5f1a
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/BillingMode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+
+public interface BillingMode {
+
+ List<RecurringInvoiceItemData> calculateInvoiceItemData(LocalDate startDate, @Nullable LocalDate endDate, LocalDate targetDate,
+ int billingCycleDay, BillingPeriod billingPeriod) throws InvalidDateSequenceException;
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/CreditAdjInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/CreditAdjInvoiceItem.java
new file mode 100644
index 0000000..270b186
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/CreditAdjInvoiceItem.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public class CreditAdjInvoiceItem extends AdjInvoiceItem {
+
+ public CreditAdjInvoiceItem(final UUID invoiceId, final UUID accountId, final LocalDate date,
+ final BigDecimal amount, final Currency currency) {
+ this(UUID.randomUUID(), null, invoiceId, accountId, date, amount, currency);
+ }
+
+ public CreditAdjInvoiceItem(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, final LocalDate date,
+ final BigDecimal amount, final Currency currency) {
+ super(id, createdDate, invoiceId, accountId, date, date, amount, currency);
+ }
+
+ @Override
+ public InvoiceItemType getInvoiceItemType() {
+ return InvoiceItemType.CREDIT_ADJ;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Invoice adjustment";
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/CreditBalanceAdjInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/CreditBalanceAdjInvoiceItem.java
new file mode 100644
index 0000000..c046ba3
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/CreditBalanceAdjInvoiceItem.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public class CreditBalanceAdjInvoiceItem extends AdjInvoiceItem {
+
+ public CreditBalanceAdjInvoiceItem(final UUID invoiceId, final UUID accountId,
+ final LocalDate date, final BigDecimal amount, final Currency currency) {
+ this(UUID.randomUUID(), null, invoiceId, accountId, date, null, amount, currency);
+ }
+
+ public CreditBalanceAdjInvoiceItem(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId,
+ final LocalDate date, final UUID linkedInvoiceItemId,
+ final BigDecimal amount, final Currency currency) {
+ super(id, createdDate, invoiceId, accountId, date, date, amount, currency, linkedInvoiceItemId);
+ }
+
+ @Override
+ public InvoiceItemType getInvoiceItemType() {
+ return InvoiceItemType.CBA_ADJ;
+ }
+
+ @Override
+ public String getDescription() {
+ final String secondDescription;
+ if (getAmount().compareTo(BigDecimal.ZERO) >= 0) {
+ secondDescription = "account credit";
+ } else {
+ secondDescription = "use of account credit";
+ }
+ return String.format("Adjustment (%s)", secondDescription);
+ }
+}
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
new file mode 100644
index 0000000..e95d4eb
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.calculator.InvoiceCalculatorUtils;
+import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
+import org.killbill.billing.invoice.dao.InvoiceModelDao;
+import org.killbill.billing.invoice.dao.InvoicePaymentModelDao;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+
+public class DefaultInvoice extends EntityBase implements Invoice {
+
+ private final List<InvoiceItem> invoiceItems = new ArrayList<InvoiceItem>();
+ private final List<InvoicePayment> payments = new ArrayList<InvoicePayment>();
+ private final UUID accountId;
+ private final Integer invoiceNumber;
+ private final LocalDate invoiceDate;
+ private final LocalDate targetDate;
+ private final Currency currency;
+ private final boolean migrationInvoice;
+
+ private final Currency processedCurrency;
+
+ // Used to create a new invoice
+ public DefaultInvoice(final UUID accountId, final LocalDate invoiceDate, final LocalDate targetDate, final Currency currency) {
+ this(UUID.randomUUID(), accountId, null, invoiceDate, targetDate, currency, false);
+ }
+
+ public DefaultInvoice(final UUID invoiceId, final UUID accountId, @Nullable final Integer invoiceNumber, final LocalDate invoiceDate,
+ final LocalDate targetDate, final Currency currency, final boolean isMigrationInvoice) {
+ this(invoiceId, null, accountId, invoiceNumber, invoiceDate, targetDate, currency, currency, isMigrationInvoice);
+ }
+
+ // Used to hydrate invoice from persistence layer
+ public DefaultInvoice(final UUID invoiceId, @Nullable final DateTime createdDate, final UUID accountId,
+ @Nullable final Integer invoiceNumber, final LocalDate invoiceDate,
+ final LocalDate targetDate, final Currency currency, final Currency processedCurrency, final boolean isMigrationInvoice) {
+ super(invoiceId, createdDate, createdDate);
+ this.accountId = accountId;
+ this.invoiceNumber = invoiceNumber;
+ this.invoiceDate = invoiceDate;
+ this.targetDate = targetDate;
+ this.currency = currency;
+ this.processedCurrency = processedCurrency;
+ this.migrationInvoice = isMigrationInvoice;
+ }
+
+ public DefaultInvoice(final InvoiceModelDao invoiceModelDao) {
+ this(invoiceModelDao.getId(), invoiceModelDao.getCreatedDate(), invoiceModelDao.getAccountId(),
+ invoiceModelDao.getInvoiceNumber(), invoiceModelDao.getInvoiceDate(), invoiceModelDao.getTargetDate(),
+ invoiceModelDao.getCurrency(), invoiceModelDao.getProcessedCurrency(), invoiceModelDao.isMigrated());
+ addInvoiceItems(Collections2.transform(invoiceModelDao.getInvoiceItems(), new Function<InvoiceItemModelDao, InvoiceItem>() {
+ @Override
+ public InvoiceItem apply(final InvoiceItemModelDao input) {
+ return InvoiceItemFactory.fromModelDao(input);
+ }
+ }));
+ addPayments(Collections2.transform(invoiceModelDao.getInvoicePayments(), new Function<InvoicePaymentModelDao, InvoicePayment>() {
+ @Override
+ public InvoicePayment apply(final InvoicePaymentModelDao input) {
+ return new DefaultInvoicePayment(input);
+ }
+ }));
+ }
+
+ @Override
+ public boolean addInvoiceItem(final InvoiceItem item) {
+ return invoiceItems.add(item);
+ }
+
+ @Override
+ public boolean addInvoiceItems(final Collection<InvoiceItem> items) {
+ return this.invoiceItems.addAll(items);
+ }
+
+ @Override
+ public List<InvoiceItem> getInvoiceItems() {
+ return invoiceItems;
+ }
+
+ @Override
+ public <T extends InvoiceItem> List<InvoiceItem> getInvoiceItems(final Class<T> clazz) {
+ final List<InvoiceItem> results = new ArrayList<InvoiceItem>();
+ for (final InvoiceItem item : invoiceItems) {
+ if (clazz.isInstance(item)) {
+ results.add(item);
+ }
+ }
+ return results;
+ }
+
+ @Override
+ public int getNumberOfItems() {
+ return invoiceItems.size();
+ }
+
+ @Override
+ public boolean addPayment(final InvoicePayment payment) {
+ return payments.add(payment);
+ }
+
+ @Override
+ public boolean addPayments(final Collection<InvoicePayment> payments) {
+ return this.payments.addAll(payments);
+ }
+
+ @Override
+ public List<InvoicePayment> getPayments() {
+ return payments;
+ }
+
+ @Override
+ public int getNumberOfPayments() {
+ return payments.size();
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ /**
+ * null until retrieved from the database
+ *
+ * @return the invoice number
+ */
+ @Override
+ public Integer getInvoiceNumber() {
+ return invoiceNumber;
+ }
+
+ @Override
+ public LocalDate getInvoiceDate() {
+ return invoiceDate;
+ }
+
+ @Override
+ public LocalDate getTargetDate() {
+ return targetDate;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ public Currency getProcessedCurrency() {
+ return processedCurrency;
+ }
+
+ @Override
+ public boolean isMigrationInvoice() {
+ return migrationInvoice;
+ }
+
+ @Override
+ public BigDecimal getPaidAmount() {
+ return InvoiceCalculatorUtils.computeInvoiceAmountPaid(currency, payments);
+ }
+
+ @Override
+ public BigDecimal getOriginalChargedAmount() {
+ return InvoiceCalculatorUtils.computeInvoiceOriginalAmountCharged(createdDate, currency, invoiceItems);
+ }
+
+ @Override
+ public BigDecimal getChargedAmount() {
+ return InvoiceCalculatorUtils.computeInvoiceAmountCharged(currency, invoiceItems);
+ }
+
+ @Override
+ public BigDecimal getCreditedAmount() {
+ return InvoiceCalculatorUtils.computeInvoiceAmountCredited(currency, invoiceItems);
+ }
+
+ @Override
+ public BigDecimal getRefundedAmount() {
+ return InvoiceCalculatorUtils.computeInvoiceAmountRefunded(currency, payments);
+ }
+
+ @Override
+ public BigDecimal getBalance() {
+ return InvoiceCalculatorUtils.computeInvoiceBalance(currency, invoiceItems, payments);
+ }
+
+ @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/DefaultInvoicePayment.java b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java
new file mode 100644
index 0000000..1207c34
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoicePayment.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
+import org.killbill.billing.invoice.dao.InvoicePaymentModelDao;
+import org.killbill.billing.entity.EntityBase;
+
+public class DefaultInvoicePayment extends EntityBase implements InvoicePayment {
+
+ private final UUID paymentId;
+ private final InvoicePaymentType type;
+ private final UUID invoiceId;
+ private final DateTime paymentDate;
+ private final BigDecimal amount;
+ private final Currency currency;
+ private final Currency processedCurrency;
+ private final UUID paymentCookieId;
+ private final UUID linkedInvoicePaymentId;
+
+ public DefaultInvoicePayment(final InvoicePaymentType type, final UUID paymentId, final UUID invoiceId, final DateTime paymentDate,
+ final BigDecimal amount, final Currency currency, final Currency processedCurrency) {
+ this(UUID.randomUUID(), null, type, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, null, null);
+ }
+
+ public DefaultInvoicePayment(final UUID id, final InvoicePaymentType type, final UUID paymentId, final UUID invoiceId, final DateTime paymentDate,
+ @Nullable final BigDecimal amount, @Nullable final Currency currency, @Nullable final Currency processedCurrency, @Nullable final UUID paymentCookieId,
+ @Nullable final UUID linkedInvoicePaymentId) {
+ this(id, null, type, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, paymentCookieId, linkedInvoicePaymentId);
+ }
+
+ public DefaultInvoicePayment(final UUID id, @Nullable final DateTime createdDate, final InvoicePaymentType type, final UUID paymentId, final UUID invoiceId, final DateTime paymentDate,
+ @Nullable final BigDecimal amount, @Nullable final Currency currency, @Nullable final Currency processedCurrency, @Nullable final UUID paymentCookieId,
+ @Nullable final UUID linkedInvoicePaymentId) {
+ super(id, createdDate, createdDate);
+ this.type = type;
+ this.paymentId = paymentId;
+ this.amount = amount;
+ this.invoiceId = invoiceId;
+ this.paymentDate = paymentDate;
+ this.currency = currency;
+ this.processedCurrency =processedCurrency;
+ this.paymentCookieId = paymentCookieId;
+ this.linkedInvoicePaymentId = linkedInvoicePaymentId;
+ }
+
+ public DefaultInvoicePayment(final InvoicePaymentModelDao invoicePaymentModelDao) {
+ this(invoicePaymentModelDao.getId(), invoicePaymentModelDao.getCreatedDate(), invoicePaymentModelDao.getType(),
+ invoicePaymentModelDao.getPaymentId(), invoicePaymentModelDao.getInvoiceId(), invoicePaymentModelDao.getPaymentDate(),
+ invoicePaymentModelDao.getAmount(), invoicePaymentModelDao.getCurrency(), invoicePaymentModelDao.getProcessedCurrency(),
+ invoicePaymentModelDao.getPaymentCookieId(),
+ invoicePaymentModelDao.getLinkedInvoicePaymentId());
+ }
+
+ @Override
+ public InvoicePaymentType getType() {
+ return type;
+ }
+
+ @Override
+ public UUID getPaymentId() {
+ return paymentId;
+ }
+
+ @Override
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ @Override
+ public DateTime getPaymentDate() {
+ return paymentDate;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public UUID getLinkedInvoicePaymentId() {
+ return linkedInvoicePaymentId;
+ }
+
+ @Override
+ public UUID getPaymentCookieId() {
+ return paymentCookieId;
+ }
+
+ @Override
+ public Currency getProcessedCurrency() {
+ return processedCurrency;
+ }
+
+}
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
new file mode 100644
index 0000000..715030f
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/ExternalChargeInvoiceItem.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public class ExternalChargeInvoiceItem extends InvoiceItemBase {
+
+ public ExternalChargeInvoiceItem(final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId, @Nullable final String description,
+ final LocalDate date, final BigDecimal amount, final Currency currency) {
+ this(UUID.randomUUID(), invoiceId, accountId, bundleId, description, date, amount, currency);
+ }
+
+ public ExternalChargeInvoiceItem(final UUID id, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
+ @Nullable final String description, final LocalDate date, final BigDecimal amount, final Currency currency) {
+ this(id, null, invoiceId, accountId, bundleId, description, date, amount, currency);
+ }
+
+ 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 date, final BigDecimal amount, final Currency currency) {
+ super(id, createdDate, invoiceId, accountId, bundleId, null, description, null, date, null, amount, currency);
+ }
+
+ @Override
+ public String getDescription() {
+ if (getPlanName() == null) {
+ return "External charge";
+ } else {
+ return String.format("%s (external charge)", getPlanName());
+ }
+ }
+
+ @Override
+ public InvoiceItemType getInvoiceItemType() {
+ return InvoiceItemType.EXTERNAL_CHARGE;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/FixedPriceInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/FixedPriceInvoiceItem.java
new file mode 100644
index 0000000..cad9f73
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/FixedPriceInvoiceItem.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public class FixedPriceInvoiceItem extends InvoiceItemBase {
+
+ public FixedPriceInvoiceItem(final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId, @Nullable final UUID subscriptionId,
+ final String planName, final String phaseName,
+ final LocalDate date, final BigDecimal amount, final Currency currency) {
+ this(UUID.randomUUID(), null, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, date, amount, currency);
+ }
+
+ public FixedPriceInvoiceItem(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 LocalDate date, final BigDecimal amount, final Currency currency) {
+ super(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, date, null, amount, currency);
+ }
+
+ @Override
+ public String getDescription() {
+ if (getPhaseName() == null) {
+ return "Fixed price charge";
+ } else {
+ if (getAmount().compareTo(BigDecimal.ZERO) == 0) {
+ return getPhaseName();
+ } else {
+ return String.format("%s (fixed price)", getPhaseName());
+ }
+ }
+ }
+
+ @Override
+ public InvoiceItemType getInvoiceItemType() {
+ return InvoiceItemType.FIXED;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InAdvanceBillingMode.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InAdvanceBillingMode.java
new file mode 100644
index 0000000..11d5aa6
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InAdvanceBillingMode.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.generator.BillingIntervalDetail;
+
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods;
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateProRationAfterLastBillingCycleDate;
+import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateProRationBeforeFirstBillingPeriod;
+
+public class InAdvanceBillingMode implements BillingMode {
+
+ private static final Logger log = LoggerFactory.getLogger(InAdvanceBillingMode.class);
+
+ @Override
+ public List<RecurringInvoiceItemData> calculateInvoiceItemData(final LocalDate startDate, @Nullable final LocalDate endDate,
+ final LocalDate targetDate,
+ final int billingCycleDayLocal, final BillingPeriod billingPeriod) throws InvalidDateSequenceException {
+ if (endDate != null && endDate.isBefore(startDate)) {
+ throw new InvalidDateSequenceException();
+ }
+ if (targetDate.isBefore(startDate)) {
+ throw new InvalidDateSequenceException();
+ }
+
+ final List<RecurringInvoiceItemData> results = new ArrayList<RecurringInvoiceItemData>();
+
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod);
+
+ // We are not billing for less than a day (we could...)
+ if (endDate != null && endDate.equals(startDate)) {
+ return results;
+ }
+ //
+ // If there is an endDate and that endDate is before our first coming firstBillingCycleDate, all we have to do
+ // is to charge for that period
+ //
+ if (endDate != null && !endDate.isAfter(billingIntervalDetail.getFirstBillingCycleDate())) {
+ final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, endDate, billingPeriod);
+ final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, endDate, leadingProRationPeriods);
+ results.add(itemData);
+ return results;
+ }
+
+ //
+ // Leading proration if
+ // i) The first firstBillingCycleDate is strictly after our start date AND
+ // ii) The endDate is is not null and is strictly after our firstBillingCycleDate (previous check)
+ //
+ if (billingIntervalDetail.getFirstBillingCycleDate().isAfter(startDate)) {
+ final BigDecimal leadingProRationPeriods = calculateProRationBeforeFirstBillingPeriod(startDate, billingIntervalDetail.getFirstBillingCycleDate(), billingPeriod);
+ if (leadingProRationPeriods != null && leadingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
+ // Not common - add info in the logs for debugging purposes
+ final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(startDate, billingIntervalDetail.getFirstBillingCycleDate(), leadingProRationPeriods);
+ log.info("Adding pro-ration: {}", itemData);
+ results.add(itemData);
+ }
+ }
+
+ //
+ // Calculate the effectiveEndDate from the firstBillingCycleDate:
+ // - If endDate != null and targetDate is after endDate => this is the endDate and will lead to a trailing pro-ration
+ // - If not, this is the last billingCycleDate calculation right after the targetDate
+ //
+ final LocalDate effectiveEndDate = billingIntervalDetail.getEffectiveEndDate();
+
+ //
+ // Based on what we calculated previously, code recompute one more time the numberOfWholeBillingPeriods
+ //
+ final LocalDate lastBillingCycleDate = billingIntervalDetail.getLastBillingCycleDate();
+ final int numberOfWholeBillingPeriods = calculateNumberOfWholeBillingPeriods(billingIntervalDetail.getFirstBillingCycleDate(), lastBillingCycleDate, billingPeriod);
+
+ for (int i = 0; i < numberOfWholeBillingPeriods; i++) {
+ final LocalDate servicePeriodStartDate;
+ if (results.size() > 0) {
+ // Make sure the periods align, especially with the pro-ration calculations above
+ servicePeriodStartDate = results.get(results.size() - 1).getEndDate();
+ } else if (i == 0) {
+ // Use the specified start date
+ servicePeriodStartDate = startDate;
+ } else {
+ throw new IllegalStateException("We should at least have one invoice item!");
+ }
+
+ // Make sure to align the end date with the BCD
+ final LocalDate servicePeriodEndDate = billingIntervalDetail.getFutureBillingDateFor(i + 1);
+ results.add(new RecurringInvoiceItemData(servicePeriodStartDate, servicePeriodEndDate, BigDecimal.ONE));
+ }
+
+ //
+ // Now we check if indeed we need a trailing proration and add that incomplete item
+ //
+ if (effectiveEndDate.isAfter(lastBillingCycleDate)) {
+ final BigDecimal trailingProRationPeriods = calculateProRationAfterLastBillingCycleDate(effectiveEndDate, lastBillingCycleDate, billingPeriod);
+ if (trailingProRationPeriods.compareTo(BigDecimal.ZERO) > 0) {
+ // Not common - add info in the logs for debugging purposes
+ final RecurringInvoiceItemData itemData = new RecurringInvoiceItemData(lastBillingCycleDate, effectiveEndDate, trailingProRationPeriods);
+ log.info("Adding trailing pro-ration: {}", itemData);
+ results.add(itemData);
+ }
+ }
+ return results;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
new file mode 100644
index 0000000..175bfec
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemBase.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.entity.EntityBase;
+
+public abstract class InvoiceItemBase extends EntityBase implements InvoiceItem {
+
+ /* Common to all items */
+ protected final UUID invoiceId;
+ protected final UUID accountId;
+ protected final LocalDate startDate;
+ protected final LocalDate endDate;
+ protected final BigDecimal amount;
+ protected final Currency currency;
+
+ /* Fixed and recurring specific */
+ protected final UUID subscriptionId;
+ protected final UUID bundleId;
+ protected final String planName;
+ protected final String phaseName;
+
+ /* Recurring specific */
+ protected final BigDecimal rate;
+
+ /* RepairAdjInvoiceItem */
+ protected final UUID linkedItemId;
+
+ @Override
+ public String toString() {
+ // Note: we don't use all fields here, as the output would be overwhelming
+ // (we output all invoice items as they are generated).
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getInvoiceItemType());
+ sb.append("{startDate=").append(startDate);
+ sb.append(", endDate=").append(endDate);
+ sb.append(", amount=").append(amount);
+ sb.append(", rate=").append(rate);
+ sb.append(", subscriptionId=").append(subscriptionId);
+ sb.append(", linkedItemId=").append(linkedItemId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ /*
+ * CTORs with ID; called from DAO when rehydrating
+ */
+ // No rate and no reversing item
+ public InvoiceItemBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
+ @Nullable final UUID subscriptionId, @Nullable final String planName, @Nullable final String phaseName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency) {
+ this(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, null, currency, null);
+ }
+
+ // With rate but no reversing item
+ public InvoiceItemBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
+ @Nullable final UUID subscriptionId, @Nullable final String planName, @Nullable final String phaseName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency) {
+ this(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, rate, currency, null);
+ }
+
+ // With reversing item, no rate
+ public InvoiceItemBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
+ @Nullable final UUID subscriptionId, @Nullable final String planName, @Nullable final String phaseName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency, final UUID reversedItemId) {
+ this(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, null, currency, reversedItemId);
+ }
+
+ private InvoiceItemBase(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId,
+ @Nullable final UUID subscriptionId, @Nullable final String planName, @Nullable final String phaseName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final BigDecimal rate, final Currency currency,
+ final UUID reversedItemId) {
+ super(id, createdDate, createdDate);
+ this.invoiceId = invoiceId;
+ this.accountId = accountId;
+ this.subscriptionId = subscriptionId;
+ this.bundleId = bundleId;
+ this.planName = planName;
+ this.phaseName = phaseName;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.amount = amount;
+ this.currency = currency;
+ this.rate = rate;
+ this.linkedItemId = reversedItemId;
+ }
+
+ @Override
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ @Override
+ public String getPlanName() {
+ return planName;
+ }
+
+ @Override
+ public String getPhaseName() {
+ return phaseName;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+
+ @Override
+ public LocalDate getEndDate() {
+ return endDate;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public BigDecimal getRate() {
+ return rate;
+ }
+
+ @Override
+ public UUID getLinkedItemId() {
+ return linkedItemId;
+ }
+
+
+ @Override
+ public boolean equals(final Object o) {
+
+ if (!matches(o)) {
+ return false;
+ }
+ final InvoiceItemBase that = (InvoiceItemBase) o;
+ if (!super.equals(that)) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (linkedItemId != null ? !linkedItemId.equals(that.linkedItemId) : that.linkedItemId != null) {
+ return false;
+ }
+ return true;
+ }
+
+
+ @Override
+ public boolean matches(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof InvoiceItemBase)) {
+ return false;
+ }
+
+ final InvoiceItemBase that = (InvoiceItemBase) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+ if (safeCompareTo(startDate, that.startDate) != 0) {
+ return false;
+ }
+ if (safeCompareTo(endDate, that.endDate) != 0) {
+ return false;
+ }
+ if (safeCompareTo(amount, that.amount) != 0) {
+ return false;
+ }
+ if (safeCompareTo(rate, that.rate) != 0) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (phaseName != null ? !phaseName.equals(that.phaseName) : that.phaseName != null) {
+ return false;
+ }
+ if (planName != null ? !planName.equals(that.planName) : that.planName != null) {
+ return false;
+ }
+ return true;
+ }
+
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (invoiceId != null ? invoiceId.hashCode() : 0);
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (startDate != null ? startDate.hashCode() : 0);
+ result = 31 * result + (endDate != null ? endDate.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (subscriptionId != null ? subscriptionId.hashCode() : 0);
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (planName != null ? planName.hashCode() : 0);
+ result = 31 * result + (phaseName != null ? phaseName.hashCode() : 0);
+ result = 31 * result + (rate != null ? rate.hashCode() : 0);
+ result = 31 * result + (linkedItemId != null ? linkedItemId.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public abstract InvoiceItemType getInvoiceItemType();
+
+ @Override
+ public abstract String getDescription();
+
+}
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
new file mode 100644
index 0000000..46538b3
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/InvoiceItemFactory.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
+
+public class InvoiceItemFactory {
+
+ private InvoiceItemFactory() {}
+
+ public static InvoiceItem fromModelDao(final InvoiceItemModelDao invoiceItemModelDao) {
+ if (invoiceItemModelDao == null) {
+ return null;
+ }
+
+ final UUID id = invoiceItemModelDao.getId();
+ final DateTime createdDate = invoiceItemModelDao.getCreatedDate();
+ final UUID invoiceId = invoiceItemModelDao.getInvoiceId();
+ final UUID accountId = invoiceItemModelDao.getAccountId();
+ final UUID bundleId = invoiceItemModelDao.getBundleId();
+ final UUID subscriptionId = invoiceItemModelDao.getSubscriptionId();
+ final String planName = invoiceItemModelDao.getPlanName();
+ final String phaseName = invoiceItemModelDao.getPhaseName();
+ final LocalDate startDate = invoiceItemModelDao.getStartDate();
+ final LocalDate endDate = invoiceItemModelDao.getEndDate();
+ final BigDecimal amount = invoiceItemModelDao.getAmount();
+ final BigDecimal rate = invoiceItemModelDao.getRate();
+ final Currency currency = invoiceItemModelDao.getCurrency();
+ final UUID linkedItemId = invoiceItemModelDao.getLinkedItemId();
+
+ final InvoiceItem item;
+ final InvoiceItemType type = invoiceItemModelDao.getType();
+ switch (type) {
+ case EXTERNAL_CHARGE:
+ item = new ExternalChargeInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, planName, startDate, amount, currency);
+ break;
+ case FIXED:
+ item = new FixedPriceInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, amount, currency);
+ break;
+ case RECURRING:
+ item = new RecurringInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, rate, currency);
+ break;
+ case CBA_ADJ:
+ item = new CreditBalanceAdjInvoiceItem(id, createdDate, invoiceId, accountId, startDate, linkedItemId, amount, currency);
+ break;
+ case CREDIT_ADJ:
+ item = new CreditAdjInvoiceItem(id, createdDate, invoiceId, accountId, startDate, amount, currency);
+ break;
+ case REFUND_ADJ:
+ item = new RefundAdjInvoiceItem(id, createdDate, invoiceId, accountId, startDate, amount, currency);
+ break;
+ case REPAIR_ADJ:
+ item = new RepairAdjInvoiceItem(id, createdDate, invoiceId, accountId, startDate, endDate, amount, currency, linkedItemId);
+ break;
+ case ITEM_ADJ:
+ item = new ItemAdjInvoiceItem(id, createdDate, invoiceId, accountId, startDate, amount, currency, linkedItemId);
+ break;
+ default:
+ throw new RuntimeException("Unexpected type of event item " + type);
+ }
+
+ return item;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/ItemAdjInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/ItemAdjInvoiceItem.java
new file mode 100644
index 0000000..7a11b4b
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/ItemAdjInvoiceItem.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public class ItemAdjInvoiceItem extends AdjInvoiceItem {
+
+ public ItemAdjInvoiceItem(final InvoiceItem invoiceItem, final LocalDate effectiveDate,
+ final BigDecimal amount, final Currency currency) {
+ this(UUID.randomUUID(), invoiceItem.getInvoiceId(), invoiceItem.getAccountId(), effectiveDate,
+ amount, currency, invoiceItem.getId());
+ }
+
+ public ItemAdjInvoiceItem(final UUID id, final UUID invoiceId, final UUID accountId, final LocalDate startDate,
+ final BigDecimal amount, final Currency currency, final UUID linkedItemId) {
+ this(id, null, invoiceId, accountId, startDate, amount, currency, linkedItemId);
+ }
+
+ public ItemAdjInvoiceItem(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, final LocalDate startDate,
+ final BigDecimal amount, final Currency currency, final UUID linkedItemId) {
+ super(id, createdDate, invoiceId, accountId, startDate, startDate, amount, currency, linkedItemId);
+ }
+
+ @Override
+ public InvoiceItemType getInvoiceItemType() {
+ return InvoiceItemType.ITEM_ADJ;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Invoice item adjustment";
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/MigrationInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/MigrationInvoiceItem.java
new file mode 100644
index 0000000..cf1095d
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/MigrationInvoiceItem.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.MigrationPlan;
+
+public class MigrationInvoiceItem extends FixedPriceInvoiceItem {
+
+ public MigrationInvoiceItem(final UUID invoiceId, final UUID accountId, final LocalDate startDate,
+ final BigDecimal amount, final Currency currency) {
+ super(invoiceId, accountId, null, null, MigrationPlan.MIGRATION_PLAN_NAME, MigrationPlan.MIGRATION_PLAN_PHASE_NAME,
+ startDate, amount, currency);
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/RecurringInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/RecurringInvoiceItem.java
new file mode 100644
index 0000000..5194814
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/RecurringInvoiceItem.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public class RecurringInvoiceItem extends InvoiceItemBase {
+
+ public RecurringInvoiceItem(final UUID invoiceId, final UUID accountId, final UUID bundleId, final UUID subscriptionId,
+ final String planName, final String phaseName, final LocalDate startDate, final LocalDate endDate,
+ final BigDecimal amount, final BigDecimal rate, final Currency currency) {
+ this(UUID.randomUUID(), null, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, rate, currency);
+ }
+
+ public RecurringInvoiceItem(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 LocalDate startDate, final LocalDate endDate,
+ final BigDecimal amount, final BigDecimal rate, final Currency currency) {
+ super(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, rate, currency);
+ }
+
+ @Override
+ public String getDescription() {
+ return phaseName;
+ }
+
+ @Override
+ public BigDecimal getRate() {
+ return rate;
+ }
+
+ @Override
+ public InvoiceItemType getInvoiceItemType() {
+ return InvoiceItemType.RECURRING;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/RecurringInvoiceItemData.java b/invoice/src/main/java/org/killbill/billing/invoice/model/RecurringInvoiceItemData.java
new file mode 100644
index 0000000..6077565
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/RecurringInvoiceItemData.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+
+public class RecurringInvoiceItemData {
+
+ private final LocalDate startDate;
+ private final LocalDate endDate;
+ private final BigDecimal numberOfCycles;
+
+ public RecurringInvoiceItemData(final LocalDate startDate, final LocalDate endDate, final BigDecimal numberOfCycles) {
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.numberOfCycles = numberOfCycles;
+ }
+
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+
+ public LocalDate getEndDate() {
+ return endDate;
+ }
+
+ public BigDecimal getNumberOfCycles() {
+ return numberOfCycles;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("RecurringInvoiceItemData");
+ sb.append("{startDate=").append(startDate);
+ sb.append(", endDate=").append(endDate);
+ sb.append(", numberOfCycles=").append(numberOfCycles);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final RecurringInvoiceItemData that = (RecurringInvoiceItemData) o;
+
+ if (endDate != null ? !endDate.equals(that.endDate) : that.endDate != null) {
+ return false;
+ }
+ if (numberOfCycles != null ? !numberOfCycles.equals(that.numberOfCycles) : that.numberOfCycles != null) {
+ return false;
+ }
+ if (startDate != null ? !startDate.equals(that.startDate) : that.startDate != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = startDate != null ? startDate.hashCode() : 0;
+ result = 31 * result + (endDate != null ? endDate.hashCode() : 0);
+ result = 31 * result + (numberOfCycles != null ? numberOfCycles.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/RefundAdjInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/RefundAdjInvoiceItem.java
new file mode 100644
index 0000000..291b3c5
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/RefundAdjInvoiceItem.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public class RefundAdjInvoiceItem extends AdjInvoiceItem {
+
+ public RefundAdjInvoiceItem(final UUID invoiceId, final UUID accountId, final LocalDate date,
+ final BigDecimal amount, final Currency currency) {
+ this(UUID.randomUUID(), null, invoiceId, accountId, date, amount, currency);
+ }
+
+ public RefundAdjInvoiceItem(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, final LocalDate date,
+ final BigDecimal amount, final Currency currency) {
+ super(id, createdDate, invoiceId, accountId, date, date, amount, currency);
+ }
+
+ @Override
+ public InvoiceItemType getInvoiceItemType() {
+ return InvoiceItemType.REFUND_ADJ;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Invoice adjustment";
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/RepairAdjInvoiceItem.java b/invoice/src/main/java/org/killbill/billing/invoice/model/RepairAdjInvoiceItem.java
new file mode 100644
index 0000000..ffe2eab
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/RepairAdjInvoiceItem.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public class RepairAdjInvoiceItem extends AdjInvoiceItem {
+
+ public RepairAdjInvoiceItem(final UUID invoiceId, final UUID accountId, final LocalDate startDate, final LocalDate endDate,
+ final BigDecimal amount, final Currency currency, final UUID reversingId) {
+ this(UUID.randomUUID(), null, invoiceId, accountId, startDate, endDate, amount, currency, reversingId);
+ }
+
+ public RepairAdjInvoiceItem(final UUID id, @Nullable final DateTime createdDate, final UUID invoiceId, final UUID accountId, final LocalDate startDate, final LocalDate endDate,
+ final BigDecimal amount, final Currency currency, final UUID reversingId) {
+ super(id, createdDate, invoiceId, accountId, startDate, endDate, amount, currency, reversingId);
+ }
+
+ @Override
+ public InvoiceItemType getInvoiceItemType() {
+ return InvoiceItemType.REPAIR_ADJ;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Adjustment (subscription change)";
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
new file mode 100644
index 0000000..25e55c1
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDateNotifier.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.invoice.InvoiceListener;
+import org.killbill.billing.invoice.api.DefaultInvoiceService;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueHandler;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.config.InvoiceConfig;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+
+import com.google.inject.Inject;
+
+public class DefaultNextBillingDateNotifier implements NextBillingDateNotifier {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultNextBillingDateNotifier.class);
+
+ public static final String NEXT_BILLING_DATE_NOTIFIER_QUEUE = "next-billing-date-queue";
+
+ private final NotificationQueueService notificationQueueService;
+ private final InvoiceConfig config;
+ private final SubscriptionBaseInternalApi subscriptionApi;
+ private final InvoiceListener listener;
+ private final InternalCallContextFactory callContextFactory;
+
+ private NotificationQueue nextBillingQueue;
+
+ @Inject
+ public DefaultNextBillingDateNotifier(final NotificationQueueService notificationQueueService,
+ final InvoiceConfig config,
+ final SubscriptionBaseInternalApi subscriptionApi,
+ final InvoiceListener listener,
+ final InternalCallContextFactory callContextFactory) {
+ this.notificationQueueService = notificationQueueService;
+ this.config = config;
+ this.subscriptionApi = subscriptionApi;
+ this.listener = listener;
+ this.callContextFactory = callContextFactory;
+ }
+
+ @Override
+ public void initialize() throws NotificationQueueAlreadyExists {
+
+ final NotificationQueueHandler notificationQueueHandler = new NotificationQueueHandler() {
+ @Override
+ public void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDate, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ try {
+ if (!(notificationKey instanceof NextBillingDateNotificationKey)) {
+ log.error("Invoice service received an unexpected event type {}", notificationKey.getClass().getName());
+ return;
+ }
+
+ final NextBillingDateNotificationKey key = (NextBillingDateNotificationKey) notificationKey;
+ try {
+ final SubscriptionBase subscription = subscriptionApi.getSubscriptionFromId(key.getUuidKey(), callContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId));
+ if (subscription == null) {
+ log.warn("Next Billing Date Notification Queue handled spurious notification (key: " + key + ")");
+ } else {
+ processEvent(key.getUuidKey(), eventDate, userToken, accountRecordId, tenantRecordId);
+ }
+ } catch (SubscriptionBaseApiException e) {
+ log.warn("Next Billing Date Notification Queue handled spurious notification (key: " + key + ")", e);
+ }
+ } catch (IllegalArgumentException e) {
+ log.error("The key returned from the NextBillingNotificationQueue is not a valid UUID", e);
+ }
+ }
+ };
+
+ nextBillingQueue = notificationQueueService.createNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
+ NEXT_BILLING_DATE_NOTIFIER_QUEUE,
+ notificationQueueHandler);
+ }
+
+ @Override
+ public void start() {
+ nextBillingQueue.startQueue();
+ }
+
+ @Override
+ public void stop() throws NoSuchNotificationQueue {
+ if (nextBillingQueue != null) {
+ nextBillingQueue.stopQueue();
+ notificationQueueService.deleteNotificationQueue(nextBillingQueue.getServiceName(), nextBillingQueue.getQueueName());
+ }
+ }
+
+ private void processEvent(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ listener.handleNextBillingDateEvent(subscriptionId, eventDateTime, userToken, accountRecordId, tenantRecordId);
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
new file mode 100644
index 0000000..58c68ae
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/DefaultNextBillingDatePoster.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.invoice.api.DefaultInvoiceService;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.inject.Inject;
+
+public class DefaultNextBillingDatePoster implements NextBillingDatePoster {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultNextBillingDatePoster.class);
+
+ private final NotificationQueueService notificationQueueService;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public DefaultNextBillingDatePoster(final NotificationQueueService notificationQueueService,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.notificationQueueService = notificationQueueService;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public void insertNextBillingNotificationFromTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final UUID accountId,
+ final UUID subscriptionId, final DateTime futureNotificationTime, final UUID userToken) {
+ final InternalCallContext context = createCallContext(accountId, userToken);
+
+ final NotificationQueue nextBillingQueue;
+ try {
+ nextBillingQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
+ DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
+ log.info("Queuing next billing date notification at {} for subscriptionId {}", futureNotificationTime.toString(), subscriptionId.toString());
+
+ nextBillingQueue.recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory.getSqlDao(), futureNotificationTime,
+ new NextBillingDateNotificationKey(subscriptionId), context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
+ } catch (NoSuchNotificationQueue e) {
+ log.error("Attempting to put items on a non-existent queue (NextBillingDateNotifier).", e);
+ } catch (IOException e) {
+ log.error("Failed to serialize notificationKey for subscriptionId {}", subscriptionId);
+ }
+ }
+
+ @Override
+ public void insertNextBillingNotification(final UUID accountId, final UUID subscriptionId, final DateTime futureNotificationTime, final UUID userToken) {
+ final InternalCallContext context = createCallContext(accountId, userToken);
+
+ final NotificationQueue nextBillingQueue;
+ try {
+ nextBillingQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME,
+ DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
+ log.info("Queuing next billing date notification at {} for subscriptionId {}", futureNotificationTime.toString(), subscriptionId.toString());
+
+ nextBillingQueue.recordFutureNotification(futureNotificationTime,
+ new NextBillingDateNotificationKey(subscriptionId), context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
+ } catch (NoSuchNotificationQueue e) {
+ log.error("Attempting to put items on a non-existent queue (NextBillingDateNotifier).", e);
+ } catch (IOException e) {
+ log.error("Failed to serialize notificationKey for subscriptionId {}", subscriptionId);
+ }
+ }
+
+ private InternalCallContext createCallContext(final UUID accountId, final UUID userToken) {
+ return internalCallContextFactory.createInternalCallContext(accountId, "NextBillingDatePoster", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/EmailInvoiceNotifier.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/EmailInvoiceNotifier.java
new file mode 100644
index 0000000..723ca8f
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/EmailInvoiceNotifier.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountEmail;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceNotifier;
+import org.killbill.billing.invoice.template.HtmlInvoiceGenerator;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+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.account.api.AccountInternalApi;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.Tag;
+
+import com.google.inject.Inject;
+
+public class EmailInvoiceNotifier implements InvoiceNotifier {
+
+ private final AccountInternalApi accountApi;
+ private final TagInternalApi tagUserApi;
+ private final HtmlInvoiceGenerator generator;
+ private final EmailConfig config;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public EmailInvoiceNotifier(final AccountInternalApi accountApi,
+ final TagInternalApi tagUserApi,
+ final HtmlInvoiceGenerator generator,
+ final EmailConfig config,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.accountApi = accountApi;
+ this.tagUserApi = tagUserApi;
+ this.generator = generator;
+ this.config = config;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public void notify(final Account account, final Invoice invoice, final TenantContext context) throws InvoiceApiException {
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(account.getId(), context);
+ final List<String> to = new ArrayList<String>();
+ to.add(account.getEmail());
+
+ final List<AccountEmail> accountEmailList = accountApi.getEmails(account.getId(), internalTenantContext);
+ final List<String> cc = new ArrayList<String>();
+ for (final AccountEmail email : accountEmailList) {
+ cc.add(email.getEmail());
+ }
+
+ // Check if this account has the MANUAL_PAY system tag
+ boolean manualPay = false;
+ final List<Tag> accountTags = tagUserApi.getTags(account.getId(), ObjectType.ACCOUNT, internalTenantContext);
+ for (final Tag tag : accountTags) {
+ if (ControlTagType.MANUAL_PAY.getId().equals(tag.getTagDefinitionId())) {
+ manualPay = true;
+ break;
+ }
+ }
+
+ final String htmlBody;
+ try {
+ htmlBody = generator.generateInvoice(account, invoice, manualPay);
+ } catch (IOException e) {
+ throw new InvoiceApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
+ }
+
+ final String subject = config.getInvoiceEmailSubject();
+
+ final EmailSender sender = new DefaultEmailSender(config);
+ try {
+ sender.sendHTMLEmail(to, cc, subject, htmlBody);
+ } catch (EmailApiException e) {
+ throw new InvoiceApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
+ } catch (IOException e) {
+ throw new InvoiceApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
+ }
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java
new file mode 100644
index 0000000..b8349c0
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotificationKey.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.util.UUID;
+
+import org.killbill.notificationq.DefaultUUIDNotificationKey;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class NextBillingDateNotificationKey extends DefaultUUIDNotificationKey {
+
+ @JsonCreator
+ public NextBillingDateNotificationKey(@JsonProperty("uuidKey") final UUID uuidKey) {
+ super(uuidKey);
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotifier.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotifier.java
new file mode 100644
index 0000000..555f73e
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDateNotifier.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists;
+
+public interface NextBillingDateNotifier {
+
+ public void initialize() throws NotificationQueueAlreadyExists, NotificationQueueAlreadyExists;
+
+ public void start();
+
+ public void stop() throws NoSuchNotificationQueue;
+
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java
new file mode 100644
index 0000000..5a63e86
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/NextBillingDatePoster.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+public interface NextBillingDatePoster {
+
+ void insertNextBillingNotificationFromTransaction(EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, UUID accountId,
+ UUID subscriptionId, DateTime futureNotificationTime, UUID userToken);
+
+ void insertNextBillingNotification(UUID accountId,
+ UUID subscriptionId, DateTime futureNotificationTime, UUID userToken);
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/NullInvoiceNotifier.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/NullInvoiceNotifier.java
new file mode 100644
index 0000000..7c372da
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/NullInvoiceNotifier.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceNotifier;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+public class NullInvoiceNotifier implements InvoiceNotifier {
+
+ @Override
+ public void notify(final Account account, final Invoice invoice, final TenantContext context) {
+ // deliberate no-op
+ }
+}
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
new file mode 100644
index 0000000..b741c56
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.template.formatters;
+
+import java.math.BigDecimal;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.currency.api.CurrencyConversion;
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.currency.api.CurrencyConversionException;
+import org.killbill.billing.currency.api.Rate;
+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.InvoicePayment;
+import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
+import org.killbill.billing.invoice.model.CreditAdjInvoiceItem;
+import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
+import org.killbill.billing.invoice.model.DefaultInvoice;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
+import static org.killbill.billing.util.DefaultAmountFormatter.round;
+
+/**
+ * Format invoice fields
+ */
+public class DefaultInvoiceFormatter implements InvoiceFormatter {
+
+ private final static Logger logger = LoggerFactory.getLogger(DefaultInvoiceFormatter.class);
+
+ private final TranslatorConfig config;
+ private final Invoice invoice;
+ private final DateTimeFormatter dateFormatter;
+ private final Locale locale;
+ private final CurrencyConversionApi currencyConversionApi;
+
+ public DefaultInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, final CurrencyConversionApi currencyConversionApi) {
+ this.config = config;
+ this.invoice = invoice;
+ dateFormatter = DateTimeFormat.mediumDate().withLocale(locale);
+ this.locale = locale;
+ this.currencyConversionApi = currencyConversionApi;
+ }
+
+ @Override
+ public Integer getInvoiceNumber() {
+ return Objects.firstNonNull(invoice.getInvoiceNumber(), 0);
+ }
+
+ @Override
+ public List<InvoiceItem> getInvoiceItems() {
+ final List<InvoiceItem> invoiceItems = new ArrayList<InvoiceItem>();
+
+ InvoiceItem mergedCBAItem = null;
+ InvoiceItem mergedInvoiceAdjustment = null;
+ for (final InvoiceItem item : invoice.getInvoiceItems()) {
+ if (InvoiceItemType.CBA_ADJ.equals(item.getInvoiceItemType())) {
+ // Merge CBA items to avoid confusing the customer, since these are internal
+ // adjustments (auto generated)
+ mergedCBAItem = mergeCBAItem(invoiceItems, mergedCBAItem, item);
+ } else if (InvoiceItemType.REFUND_ADJ.equals(item.getInvoiceItemType()) ||
+ InvoiceItemType.CREDIT_ADJ.equals(item.getInvoiceItemType())) {
+ // Merge refund adjustments and credit adjustments, as these are both
+ // the same for the customer (invoice adjustment)
+ mergedInvoiceAdjustment = mergeInvoiceAdjustmentItem(invoiceItems, mergedInvoiceAdjustment, item);
+ } else {
+ invoiceItems.add(item);
+ }
+ }
+ // Don't display adjustments of zero
+ if (mergedCBAItem != null && mergedCBAItem.getAmount().compareTo(BigDecimal.ZERO) != 0) {
+ invoiceItems.add(mergedCBAItem);
+ }
+ if (mergedInvoiceAdjustment != null && mergedInvoiceAdjustment.getAmount().compareTo(BigDecimal.ZERO) != 0) {
+ invoiceItems.add(mergedInvoiceAdjustment);
+ }
+
+ final List<InvoiceItem> formatters = new ArrayList<InvoiceItem>();
+ for (final InvoiceItem item : invoiceItems) {
+ formatters.add(new DefaultInvoiceItemFormatter(config, item, dateFormatter, locale));
+ }
+ return formatters;
+ }
+
+ private InvoiceItem mergeCBAItem(final List<InvoiceItem> invoiceItems, InvoiceItem mergedCBAItem, final InvoiceItem item) {
+ if (mergedCBAItem == null) {
+ mergedCBAItem = item;
+ } else {
+ // This is really just to be safe - they should always have the same currency
+ if (!mergedCBAItem.getCurrency().equals(item.getCurrency())) {
+ invoiceItems.add(item);
+ } else {
+ mergedCBAItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), invoice.getInvoiceDate(),
+ mergedCBAItem.getAmount().add(item.getAmount()), mergedCBAItem.getCurrency());
+ }
+ }
+ return mergedCBAItem;
+ }
+
+ private InvoiceItem mergeInvoiceAdjustmentItem(final List<InvoiceItem> invoiceItems, InvoiceItem mergedInvoiceAdjustment, final InvoiceItem item) {
+ if (mergedInvoiceAdjustment == null) {
+ mergedInvoiceAdjustment = item;
+ } else {
+ // This is really just to be safe - they should always have the same currency
+ if (!mergedInvoiceAdjustment.getCurrency().equals(item.getCurrency())) {
+ invoiceItems.add(item);
+ } else {
+ mergedInvoiceAdjustment = new CreditAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), invoice.getInvoiceDate(),
+ mergedInvoiceAdjustment.getAmount().add(item.getAmount()), mergedInvoiceAdjustment.getCurrency());
+ }
+ }
+ return mergedInvoiceAdjustment;
+ }
+
+ @Override
+ public boolean addInvoiceItem(final InvoiceItem item) {
+ return invoice.addInvoiceItem(item);
+ }
+
+ @Override
+ public boolean addInvoiceItems(final Collection<InvoiceItem> items) {
+ return invoice.addInvoiceItems(items);
+ }
+
+ @Override
+ public <T extends InvoiceItem> List<InvoiceItem> getInvoiceItems(final Class<T> clazz) {
+ return Objects.firstNonNull(invoice.getInvoiceItems(clazz), ImmutableList.<InvoiceItem>of());
+ }
+
+ @Override
+ public int getNumberOfItems() {
+ return invoice.getNumberOfItems();
+ }
+
+ @Override
+ public boolean addPayment(final InvoicePayment payment) {
+ return invoice.addPayment(payment);
+ }
+
+ @Override
+ public boolean addPayments(final Collection<InvoicePayment> payments) {
+ return invoice.addPayments(payments);
+ }
+
+ @Override
+ public List<InvoicePayment> getPayments() {
+ return Objects.firstNonNull(invoice.getPayments(), ImmutableList.<InvoicePayment>of());
+ }
+
+ @Override
+ public int getNumberOfPayments() {
+ return invoice.getNumberOfPayments();
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return invoice.getAccountId();
+ }
+
+ @Override
+ public BigDecimal getChargedAmount() {
+ return round(Objects.firstNonNull(invoice.getChargedAmount(), BigDecimal.ZERO));
+ }
+
+ @Override
+ public BigDecimal getOriginalChargedAmount() {
+ return round(Objects.firstNonNull(invoice.getOriginalChargedAmount(), BigDecimal.ZERO));
+ }
+
+ @Override
+ public BigDecimal getBalance() {
+ return round(Objects.firstNonNull(invoice.getBalance(), BigDecimal.ZERO));
+ }
+
+ @Override
+ public String getFormattedChargedAmount() {
+ final NumberFormat number = NumberFormat.getCurrencyInstance(locale);
+ return number.format(getChargedAmount().doubleValue());
+ }
+
+ @Override
+ public String getFormattedPaidAmount() {
+ final NumberFormat number = NumberFormat.getCurrencyInstance(locale);
+ return number.format(getPaidAmount().doubleValue());
+ }
+
+ @Override
+ public String getFormattedBalance() {
+ final NumberFormat number = NumberFormat.getCurrencyInstance(locale);
+ return number.format(getBalance().doubleValue());
+ }
+
+ @Override
+ public Currency getProcessedCurrency() {
+ final Currency processedCurrency = ((DefaultInvoice) invoice).getProcessedCurrency();
+ // If the processed currency is different we return it; otherwise we return null so that template does not print anything special
+ return (processedCurrency != getCurrency()) ? processedCurrency : null;
+ }
+
+ @Override
+ public String getProcessedPaymentRate() {
+ final Currency currency = getProcessedCurrency();
+ if (currency == null) {
+ return null;
+ }
+ // If there were multiple payments (and refunds) we pick chose the last one
+ DateTime latestPaymentDate = null;
+ final Iterator<InvoicePayment> paymentIterator = ((DefaultInvoice) invoice).getPayments().iterator();
+ while (paymentIterator.hasNext()) {
+ final InvoicePayment cur = paymentIterator.next();
+ latestPaymentDate = latestPaymentDate != null && latestPaymentDate.isAfter(cur.getPaymentDate()) ?
+ latestPaymentDate : cur.getPaymentDate();
+
+ }
+ try {
+ final CurrencyConversion conversion = currencyConversionApi.getCurrencyConversion(currency, latestPaymentDate);
+ for (Rate rate : conversion.getRates()) {
+ if (rate.getCurrency() == getCurrency()) {
+ return rate.getValue().toString();
+ }
+ }
+ } catch (CurrencyConversionException e) {
+ logger.warn("Failed to retrieve currency conversion rates for currency = " + currency + " and date = " + latestPaymentDate, e);
+ return null;
+ }
+ logger.warn("Failed to retrieve currency conversion rates for currency = " + currency + " and date = " + latestPaymentDate);
+ return null;
+ }
+
+ @Override
+ public boolean isMigrationInvoice() {
+ return invoice.isMigrationInvoice();
+ }
+
+ @Override
+ public LocalDate getInvoiceDate() {
+ return invoice.getInvoiceDate();
+ }
+
+ @Override
+ public LocalDate getTargetDate() {
+ return invoice.getTargetDate();
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return invoice.getCurrency();
+ }
+
+ @Override
+ public BigDecimal getPaidAmount() {
+ return round(Objects.firstNonNull(invoice.getPaidAmount(), BigDecimal.ZERO));
+ }
+
+ @Override
+ public String getFormattedInvoiceDate() {
+ final LocalDate invoiceDate = invoice.getInvoiceDate();
+ if (invoiceDate == null) {
+ return "";
+ } else {
+ return Strings.nullToEmpty(invoiceDate.toString(dateFormatter));
+ }
+ }
+
+ @Override
+ public UUID getId() {
+ return invoice.getId();
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return invoice.getCreatedDate();
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return invoice.getUpdatedDate();
+ }
+
+ // Expose the fields for children classes. This is useful for further customization of the invoices
+
+ @SuppressWarnings("UnusedDeclaration")
+ protected TranslatorConfig getConfig() {
+ return config;
+ }
+
+ @SuppressWarnings("UnusedDeclaration")
+ protected DateTimeFormatter getDateFormatter() {
+ return dateFormatter;
+ }
+
+ @SuppressWarnings("UnusedDeclaration")
+ protected Locale getLocale() {
+ return locale;
+ }
+
+ protected Invoice getInvoice() {
+ return invoice;
+ }
+
+ @Override
+ public BigDecimal getCreditedAmount() {
+ return round(Objects.firstNonNull(invoice.getCreditedAmount(), BigDecimal.ZERO));
+ }
+
+ @Override
+ public BigDecimal getRefundedAmount() {
+ return round(Objects.firstNonNull(invoice.getRefundedAmount(), BigDecimal.ZERO));
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java
new file mode 100644
index 0000000..b1212aa
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.template.formatters;
+
+import java.util.Locale;
+
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
+import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+public class DefaultInvoiceFormatterFactory implements InvoiceFormatterFactory {
+
+ @Override
+ public InvoiceFormatter createInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, CurrencyConversionApi currencyConversionApi) {
+ return new DefaultInvoiceFormatter(config, invoice, locale, currencyConversionApi);
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
new file mode 100644
index 0000000..45b1f53
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.template.formatters;
+
+import java.math.BigDecimal;
+import java.text.NumberFormat;
+import java.util.Locale;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.joda.time.format.DateTimeFormatter;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.api.formatters.InvoiceItemFormatter;
+import org.killbill.billing.util.template.translation.DefaultCatalogTranslator;
+import org.killbill.billing.util.template.translation.Translator;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+
+import static org.killbill.billing.util.DefaultAmountFormatter.round;
+
+/**
+ * Format invoice item fields
+ */
+public class DefaultInvoiceItemFormatter implements InvoiceItemFormatter {
+
+ private final Translator translator;
+
+ private final InvoiceItem item;
+ private final DateTimeFormatter dateFormatter;
+ private final Locale locale;
+
+ public DefaultInvoiceItemFormatter(final TranslatorConfig config, final InvoiceItem item, final DateTimeFormatter dateFormatter, final Locale locale) {
+ this.item = item;
+ this.dateFormatter = dateFormatter;
+ this.locale = locale;
+
+ this.translator = new DefaultCatalogTranslator(config);
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return round(Objects.firstNonNull(item.getAmount(), BigDecimal.ZERO));
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return item.getCurrency();
+ }
+
+ @Override
+ public String getFormattedAmount() {
+ final NumberFormat number = NumberFormat.getCurrencyInstance(locale);
+ number.setCurrency(java.util.Currency.getInstance(item.getCurrency().toString()));
+ return number.format(getAmount().doubleValue());
+ }
+
+ @Override
+ public InvoiceItemType getInvoiceItemType() {
+ return item.getInvoiceItemType();
+ }
+
+ @Override
+ public String getDescription() {
+ return Strings.nullToEmpty(item.getDescription());
+ }
+
+ @Override
+ public LocalDate getStartDate() {
+ return item.getStartDate();
+ }
+
+ @Override
+ public LocalDate getEndDate() {
+ return item.getEndDate();
+ }
+
+ @Override
+ public String getFormattedStartDate() {
+ return item.getStartDate().toString(dateFormatter);
+ }
+
+ @Override
+ public String getFormattedEndDate() {
+ return item.getEndDate() == null ? null : item.getEndDate().toString(dateFormatter);
+ }
+
+ @Override
+ public UUID getInvoiceId() {
+ return item.getInvoiceId();
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return item.getAccountId();
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return item.getBundleId();
+ }
+
+ @Override
+ public UUID getSubscriptionId() {
+ return item.getSubscriptionId();
+ }
+
+ @Override
+ public String getPlanName() {
+ return Strings.nullToEmpty(translator.getTranslation(locale, item.getPlanName()));
+ }
+
+ @Override
+ public String getPhaseName() {
+ return Strings.nullToEmpty(translator.getTranslation(locale, item.getPhaseName()));
+ }
+
+ @Override
+ public UUID getId() {
+ return item.getId();
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return item.getCreatedDate();
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return item.getUpdatedDate();
+ }
+
+ @Override
+ public BigDecimal getRate() {
+ return round(BigDecimal.ZERO);
+ }
+
+ @Override
+ public UUID getLinkedItemId() {
+ return null;
+ }
+
+ @Override
+ public boolean matches(final Object other) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java
new file mode 100644
index 0000000..7ca43a4
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.template;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
+import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
+import org.killbill.billing.invoice.template.translator.DefaultInvoiceTranslator;
+import org.killbill.billing.util.LocaleUtils;
+import org.killbill.billing.util.email.templates.TemplateEngine;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+import com.google.inject.Inject;
+
+public class HtmlInvoiceGenerator {
+
+ private final InvoiceFormatterFactory factory;
+ private final TemplateEngine templateEngine;
+ private final TranslatorConfig config;
+ private final CurrencyConversionApi currencyConversionApi;
+
+ @Inject
+ public HtmlInvoiceGenerator(final InvoiceFormatterFactory factory, final TemplateEngine templateEngine,
+ final TranslatorConfig config, final CurrencyConversionApi currencyConversionApi) {
+ this.factory = factory;
+ this.templateEngine = templateEngine;
+ this.config = config;
+ this.currencyConversionApi = currencyConversionApi;
+ }
+
+ public String generateInvoice(final Account account, @Nullable final Invoice invoice, final boolean manualPay) throws IOException {
+ // Don't do anything if the invoice is null
+ if (invoice == null) {
+ return null;
+ }
+
+ final Map<String, Object> data = new HashMap<String, Object>();
+ final DefaultInvoiceTranslator invoiceTranslator = new DefaultInvoiceTranslator(config);
+ final Locale locale = LocaleUtils.toLocale(account.getLocale());
+ invoiceTranslator.setLocale(locale);
+ data.put("text", invoiceTranslator);
+ data.put("account", account);
+
+ final InvoiceFormatter formattedInvoice = factory.createInvoiceFormatter(config, invoice, locale, currencyConversionApi);
+ data.put("invoice", formattedInvoice);
+
+ if (manualPay) {
+ return templateEngine.executeTemplate(config.getManualPayTemplateName(), data);
+ } else {
+ return templateEngine.executeTemplate(config.getTemplateName(), data);
+ }
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/translator/DefaultInvoiceTranslator.java b/invoice/src/main/java/org/killbill/billing/invoice/template/translator/DefaultInvoiceTranslator.java
new file mode 100644
index 0000000..8c644d3
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/translator/DefaultInvoiceTranslator.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.template.translator;
+
+import java.util.Locale;
+
+import org.killbill.billing.util.template.translation.DefaultTranslatorBase;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+import com.google.inject.Inject;
+
+public class DefaultInvoiceTranslator extends DefaultTranslatorBase implements InvoiceStrings {
+
+ private Locale locale;
+
+ @Inject
+ public DefaultInvoiceTranslator(final TranslatorConfig config) {
+ super(config);
+ }
+
+ public void setLocale(final Locale locale) {
+ this.locale = locale;
+ }
+
+ @Override
+ protected String getBundlePath() {
+ return config.getInvoiceTemplateBundlePath();
+ }
+
+ @Override
+ protected String getTranslationType() {
+ return "invoice";
+ }
+
+ @Override
+ public String getInvoiceTitle() {
+ return getTranslation(locale, "invoiceTitle");
+ }
+
+ @Override
+ public String getInvoiceDate() {
+ return getTranslation(locale, "invoiceDate");
+ }
+
+ @Override
+ public String getInvoiceNumber() {
+ return getTranslation(locale, "invoiceNumber");
+ }
+
+ @Override
+ public String getAccountOwnerName() {
+ return getTranslation(locale, "accountOwnerName");
+ }
+
+ @Override
+ public String getAccountOwnerEmail() {
+ return getTranslation(locale, "accountOwnerEmail");
+ }
+
+ @Override
+ public String getAccountOwnerPhone() {
+ return getTranslation(locale, "accountOwnerPhone");
+ }
+
+ @Override
+ public String getCompanyName() {
+ return getTranslation(locale, "companyName");
+ }
+
+ @Override
+ public String getCompanyAddress() {
+ return getTranslation(locale, "companyAddress");
+ }
+
+ @Override
+ public String getCompanyCityProvincePostalCode() {
+ return getTranslation(locale, "companyCityProvincePostalCode");
+ }
+
+ @Override
+ public String getCompanyCountry() {
+ return getTranslation(locale, "companyCountry");
+ }
+
+ @Override
+ public String getCompanyUrl() {
+ return getTranslation(locale, "companyUrl");
+ }
+
+ @Override
+ public String getInvoiceItemBundleName() {
+ return getTranslation(locale, "invoiceItemBundleName");
+ }
+
+ @Override
+ public String getInvoiceItemDescription() {
+ return getTranslation(locale, "invoiceItemDescription");
+ }
+
+ @Override
+ public String getInvoiceItemServicePeriod() {
+ return getTranslation(locale, "invoiceItemServicePeriod");
+ }
+
+ @Override
+ public String getInvoiceItemAmount() {
+ return getTranslation(locale, "invoiceItemAmount");
+ }
+
+ @Override
+ public String getInvoiceAmount() {
+ return getTranslation(locale, "invoiceAmount");
+ }
+
+ @Override
+ public String getInvoiceAmountPaid() {
+ return getTranslation(locale, "invoiceAmountPaid");
+ }
+
+ @Override
+ public String getInvoiceBalance() {
+ return getTranslation(locale, "invoiceBalance");
+ }
+
+ @Override
+ public String getProcessedPaymentCurrency() {
+ return getTranslation(locale, "processedPaymentCurrency");
+ }
+
+ @Override
+ public String getProcessedPaymentRate() {
+ return getTranslation(locale, "processedPaymentRate");
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/translator/InvoiceStrings.java b/invoice/src/main/java/org/killbill/billing/invoice/template/translator/InvoiceStrings.java
new file mode 100644
index 0000000..624ff76
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/translator/InvoiceStrings.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.template.translator;
+
+public interface InvoiceStrings {
+
+ String getInvoiceTitle();
+
+ String getInvoiceDate();
+
+ String getInvoiceNumber();
+
+ String getAccountOwnerName();
+
+ String getAccountOwnerEmail();
+
+ String getAccountOwnerPhone();
+
+ // company name and address
+ String getCompanyName();
+
+ String getCompanyAddress();
+
+ String getCompanyCityProvincePostalCode();
+
+ String getCompanyCountry();
+
+ String getCompanyUrl();
+
+ String getInvoiceItemBundleName();
+
+ String getInvoiceItemDescription();
+
+ String getInvoiceItemServicePeriod();
+
+ String getInvoiceItemAmount();
+
+ String getInvoiceAmount();
+
+ String getInvoiceAmountPaid();
+
+ String getInvoiceBalance();
+
+ String getProcessedPaymentCurrency();
+
+ String getProcessedPaymentRate();
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/AccountItemTree.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/AccountItemTree.java
new file mode 100644
index 0000000..57c6bb1
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/AccountItemTree.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tree;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+/**
+ * Tree of invoice items for a given account.
+ * <p/>
+ * <p>It contains a map of <tt>SubscriptionItemTree</tt> and the logic is executed independently for all items
+ * associated to a given subscription. That also means that invoice item adjustment which cross subscriptions
+ * can't be correctly handled when they compete with other forms of adjustments.
+ * <p/>
+ * <p>The class is not thread safe, there is no such use case today, and there is a lifecyle to respect:
+ * <ul>
+ * <li>Add existing invoice items
+ * <li>Build the tree,
+ * <li>Merge the proposed list
+ * <li>Retrieves final list
+ * <ul/>
+ */
+public class AccountItemTree {
+
+ private final UUID accountId;
+ private final Map<UUID, SubscriptionItemTree> subscriptionItemTree;
+ private final List<InvoiceItem> allExistingItems;
+ private List<InvoiceItem> pendingItemAdj;
+
+ private boolean isBuilt;
+
+ public AccountItemTree(final UUID accountId) {
+ this.accountId = accountId;
+ this.subscriptionItemTree = new HashMap<UUID, SubscriptionItemTree>();
+ this.isBuilt = false;
+ this.allExistingItems = new LinkedList<InvoiceItem>();
+ this.pendingItemAdj = new LinkedList<InvoiceItem>();
+ }
+
+ /**
+ * build the subscription trees after they have been populated with existing items on disk
+ */
+ public void build() {
+ Preconditions.checkState(!isBuilt);
+
+ if (pendingItemAdj.size() > 0) {
+ for (InvoiceItem item : pendingItemAdj) {
+ addExistingItem(item, true);
+ }
+ pendingItemAdj.clear();
+ }
+ for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+ tree.build();
+ }
+ isBuilt = true;
+ }
+
+ /**
+ * Populate tree from existing items on disk
+ *
+ * @param existingItem an item read on disk
+ */
+ public void addExistingItem(final InvoiceItem existingItem) {
+ addExistingItem(existingItem, false);
+ }
+
+ private void addExistingItem(final InvoiceItem existingItem, boolean failOnMissingSubscription) {
+
+ Preconditions.checkState(!isBuilt);
+ switch (existingItem.getInvoiceItemType()) {
+ case EXTERNAL_CHARGE:
+ case CBA_ADJ:
+ case CREDIT_ADJ:
+ case REFUND_ADJ:
+ return;
+
+ case RECURRING:
+ case REPAIR_ADJ:
+ case FIXED:
+ case ITEM_ADJ:
+ break;
+
+ default:
+ Preconditions.checkState(false, "Unknown invoice item type " + existingItem.getInvoiceItemType());
+
+ }
+
+ allExistingItems.add(existingItem);
+
+ final UUID subscriptionId = getSubscriptionId(existingItem, allExistingItems);
+ Preconditions.checkState(subscriptionId != null || !failOnMissingSubscription);
+
+ if (subscriptionId == null && existingItem.getInvoiceItemType() == InvoiceItemType.ITEM_ADJ) {
+ pendingItemAdj.add(existingItem);
+ return;
+ }
+
+ if (!subscriptionItemTree.containsKey(subscriptionId)) {
+ subscriptionItemTree.put(subscriptionId, new SubscriptionItemTree(subscriptionId));
+ }
+ final SubscriptionItemTree tree = subscriptionItemTree.get(subscriptionId);
+ tree.addItem(existingItem);
+ }
+
+ /**
+ * Rebuild the new tree by merging current on-disk existing view with new proposed list.
+ *
+ * @param proposedItems list of proposed item that should be merged with current existing view
+ */
+ public void mergeWithProposedItems(final List<InvoiceItem> proposedItems) {
+
+ build();
+ for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+ tree.flatten(true);
+ }
+
+ for (InvoiceItem item : proposedItems) {
+ final UUID subscriptionId = getSubscriptionId(item, null);
+ SubscriptionItemTree tree = subscriptionItemTree.get(subscriptionId);
+ if (tree == null) {
+ tree = new SubscriptionItemTree(subscriptionId);
+ subscriptionItemTree.put(subscriptionId, tree);
+ }
+ tree.mergeProposedItem(item);
+ }
+
+ for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+ tree.buildForMerge();
+ }
+ }
+
+ /**
+ * @return the resulting list of items that should be written to disk
+ */
+ public List<InvoiceItem> getResultingItemList() {
+ final List<InvoiceItem> result = new ArrayList<InvoiceItem>();
+ for (SubscriptionItemTree tree : subscriptionItemTree.values()) {
+ final List<InvoiceItem> simplifiedView = tree.getView();
+ if (simplifiedView.size() > 0) {
+ result.addAll(simplifiedView);
+ }
+ }
+ return result;
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ private UUID getSubscriptionId(final InvoiceItem item, final List<InvoiceItem> allItems) {
+ if (item.getInvoiceItemType() == InvoiceItemType.RECURRING ||
+ item.getInvoiceItemType() == InvoiceItemType.FIXED) {
+ return item.getSubscriptionId();
+ } else {
+ final InvoiceItem linkedItem = Iterables.tryFind(allItems, new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(final InvoiceItem input) {
+ return item.getLinkedItemId().equals(input.getId());
+ }
+ }).orNull();
+ return linkedItem != null ? linkedItem.getSubscriptionId() : null;
+ }
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
new file mode 100644
index 0000000..b93fb74
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tree;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.Days;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.generator.InvoiceDateUtils;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+import org.killbill.billing.invoice.model.RepairAdjInvoiceItem;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+/**
+ * An generic invoice item that contains all pertinent fields regarding of its InvoiceItemType.
+ * <p/>
+ * It contains an action that determines what to do when building the tree (whether in normal or merge mode). It also
+ * keeps track of current adjusted and repair amount so subsequent repair can be limited to what is left.
+ */
+public class Item {
+
+ private final UUID id;
+ private final UUID accountId;
+ private final UUID bundleId;
+ private final UUID subscriptionId;
+ private final UUID invoiceId;
+ private final String planName;
+ private final String phaseName;
+ private final LocalDate startDate;
+ private final LocalDate endDate;
+ private final BigDecimal amount;
+ private final BigDecimal rate;
+ private final Currency currency;
+ private final DateTime createdDate;
+ private final UUID linkedId;
+
+ private BigDecimal currentRepairedAmount;
+ private BigDecimal adjustedAmount;
+
+ private final ItemAction action;
+
+ public enum ItemAction {
+ ADD,
+ CANCEL
+ }
+
+ public Item(final Item item, final ItemAction action) {
+ this.id = item.id;
+ this.accountId = item.accountId;
+ this.bundleId = item.bundleId;
+ this.subscriptionId = item.subscriptionId;
+ this.invoiceId = item.invoiceId;
+ this.planName = item.planName;
+ this.phaseName = item.phaseName;
+ this.startDate = item.startDate;
+ this.endDate = item.endDate;
+ this.amount = item.amount;
+ this.rate = item.rate;
+ this.currency = item.currency;
+ // In merge mode, the reverse item needs to correctly point to itself (repair of original item)
+ this.linkedId = action == ItemAction.ADD ? item.linkedId : this.id;
+ this.createdDate = item.createdDate;
+ this.currentRepairedAmount = item.currentRepairedAmount;
+ this.adjustedAmount = item.adjustedAmount;
+
+ this.action = action;
+ }
+
+ public Item(final InvoiceItem item, final ItemAction action) {
+ this.id = item.getId();
+ this.accountId = item.getAccountId();
+ this.bundleId = item.getBundleId();
+ this.subscriptionId = item.getSubscriptionId();
+ this.invoiceId = item.getInvoiceId();
+ this.planName = item.getPlanName();
+ this.phaseName = item.getPhaseName();
+ this.startDate = item.getStartDate();
+ this.endDate = item.getEndDate();
+ this.amount = item.getAmount().abs();
+ this.rate = item.getRate();
+ this.currency = item.getCurrency();
+ this.linkedId = item.getLinkedItemId();
+ this.createdDate = item.getCreatedDate();
+ this.action = action;
+
+ this.currentRepairedAmount = BigDecimal.ZERO;
+ this.adjustedAmount = BigDecimal.ZERO;
+ }
+
+ public InvoiceItem toInvoiceItem() {
+ return toProratedInvoiceItem(startDate, endDate);
+ }
+
+ public InvoiceItem toProratedInvoiceItem(final LocalDate newStartDate, final LocalDate newEndDate) {
+
+ int nbTotalDays = Days.daysBetween(startDate, endDate).getDays();
+ final boolean prorated = !(newStartDate.compareTo(startDate) == 0 && newEndDate.compareTo(endDate) == 0);
+
+ // Pro-ration is built by using the startDate, endDate and amount of this item instead of using the rate and a potential full period.
+ final BigDecimal positiveAmount = prorated ? InvoiceDateUtils.calculateProrationBetweenDates(newStartDate, newEndDate, nbTotalDays)
+ .multiply(amount) : amount;
+
+ if (action == ItemAction.ADD) {
+ return new RecurringInvoiceItem(id, createdDate, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, newStartDate, newEndDate, KillBillMoney.of(positiveAmount, currency), rate, currency);
+ } else {
+ // We first compute the maximum amount after adjustment and that sets the amount limit of how much can be repaired.
+ final BigDecimal maxAvailableAmountAfterAdj = amount.subtract(adjustedAmount);
+ final BigDecimal maxAvailableAmountForRepair = maxAvailableAmountAfterAdj.subtract(currentRepairedAmount);
+ final BigDecimal positiveAmountForRepair = positiveAmount.compareTo(maxAvailableAmountForRepair) <= 0 ? positiveAmount : maxAvailableAmountForRepair;
+ return positiveAmountForRepair.compareTo(BigDecimal.ZERO) > 0 ? new RepairAdjInvoiceItem(invoiceId, accountId, newStartDate, newEndDate, KillBillMoney.of(positiveAmountForRepair.negate(), currency), currency, linkedId) : null;
+ }
+ }
+
+ public void incrementAdjustedAmount(final BigDecimal increment) {
+ Preconditions.checkState(increment.compareTo(BigDecimal.ZERO) > 0);
+ adjustedAmount = adjustedAmount.add(increment);
+ }
+
+ public void incrementCurrentRepairedAmount(final BigDecimal increment) {
+ Preconditions.checkState(increment.compareTo(BigDecimal.ZERO) > 0);
+ currentRepairedAmount = currentRepairedAmount.add(increment);
+ }
+
+ public ItemAction getAction() {
+ return action;
+ }
+
+ public UUID getLinkedId() {
+ return linkedId;
+ }
+
+ public LocalDate getEndDate() {
+ return endDate;
+ }
+
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ /**
+ * Compare two items to check whether there are the same kind; that is whether or not they build for the same product/plan.
+ *
+ * @param other item to compare with
+ * @return
+ */
+ public boolean isSameKind(final Item other) {
+
+ final InvoiceItem otherItem = other.toInvoiceItem();
+
+ return !id.equals(otherItem.getId()) &&
+ // Finally, for the tricky part... In case of complete repairs, the new invoiceItem will always meet all of the
+ // following conditions: same type, subscription, start date. Depending on the catalog configuration, the end
+ // date check could also match (e.g. repair from annual to monthly). For that scenario, we need to default
+ // to catalog checks (the rate check is a lame check for versioned catalogs).
+ Objects.firstNonNull(planName, "").equals(Objects.firstNonNull(otherItem.getPlanName(), "")) &&
+ Objects.firstNonNull(phaseName, "").equals(Objects.firstNonNull(otherItem.getPhaseName(), "")) &&
+ Objects.firstNonNull(rate, BigDecimal.ZERO).compareTo(Objects.firstNonNull(otherItem.getRate(), BigDecimal.ZERO)) == 0;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsInterval.java
new file mode 100644
index 0000000..e4137cd
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsInterval.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tree;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.invoice.tree.Item.ItemAction;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+/**
+ * Keeps track of all the items existing on a specified interval.
+ */
+public class ItemsInterval {
+
+ private final NodeInterval interval;
+ private LinkedList<Item> items;
+
+ public ItemsInterval(final NodeInterval interval) {
+ this(interval, null);
+ }
+
+ public ItemsInterval(final NodeInterval interval, final Item initialItem) {
+ this.interval = interval;
+ this.items = Lists.newLinkedList();
+ if (initialItem != null) {
+ items.add(initialItem);
+ }
+ }
+
+ public boolean containsItem(final UUID targetId) {
+ return Iterables.tryFind(items, new Predicate<Item>() {
+ @Override
+ public boolean apply(final Item input) {
+ return input.getId().equals(targetId);
+ }
+ }).orNull() != null;
+ }
+
+ public void setAdjustment(final BigDecimal amount, final UUID targetId) {
+ final Item item = Iterables.tryFind(items, new Predicate<Item>() {
+ @Override
+ public boolean apply(final Item input) {
+ return input.getId().equals(targetId);
+ }
+ }).get();
+ item.incrementAdjustedAmount(amount);
+ }
+
+ public List<Item> getItems() {
+ return items;
+ }
+
+ public void buildForMissingInterval(final LocalDate startDate, final LocalDate endDate, final List<Item> output, final boolean addRepair) {
+ final Item item = createNewItem(startDate, endDate, addRepair);
+ if (item != null) {
+ output.add(item);
+ }
+ }
+
+ /**
+ * Determines what is left based on the mergeMode and the action for each item.
+ *
+ * @param output
+ * @param mergeMode
+ */
+ public void buildFromItems(final List<Item> output, final boolean mergeMode) {
+ final Item item = getResultingItem(mergeMode);
+ if (item != null) {
+ output.add(item);
+ }
+ }
+
+ private Item getResultingItem(final boolean mergeMode) {
+ return mergeMode ? getResulting_CANCEL_Item() : getResulting_ADD_Item();
+ }
+
+ private Item getResulting_CANCEL_Item() {
+ Preconditions.checkState(items.size() == 0 || items.size() == 1);
+ return Iterables.tryFind(items, new Predicate<Item>() {
+ @Override
+ public boolean apply(final Item input) {
+ return input.getAction() == ItemAction.CANCEL;
+ }
+ }).orNull();
+ }
+
+
+ private Item getResulting_ADD_Item() {
+
+ final Set<UUID> repairedIds = new HashSet<UUID>();
+ final ListIterator<Item> it = items.listIterator(items.size());
+
+ while (it.hasPrevious()) {
+ final Item cur = it.previous();
+ switch (cur.getAction()) {
+ case ADD:
+ // If we found a CANCEL item pointing to that item then don't return it as it was repair (full repair scenario)
+ if (!repairedIds.contains(cur.getId())) {
+ return cur;
+ }
+ break;
+
+ case CANCEL:
+ // In all cases populate the set with the id of target item being repaired
+ if (cur.getLinkedId() != null) {
+ repairedIds.add(cur.getLinkedId());
+ }
+ break;
+ }
+ }
+ return null;
+ }
+
+
+ // Just ensure that ADD items precedes CANCEL items
+ public void insertSortedItem(final Item item) {
+ items.add(item);
+ Collections.sort(items, new Comparator<Item>() {
+ @Override
+ public int compare(final Item o1, final Item o2) {
+ if (o1.getAction() == ItemAction.ADD && o2.getAction() == ItemAction.CANCEL) {
+ return -1;
+ } else if (o1.getAction() == ItemAction.CANCEL && o2.getAction() == ItemAction.ADD) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ });
+ }
+
+ public void cancelItems(final Item item) {
+ Preconditions.checkState(item.getAction() == ItemAction.ADD);
+ Preconditions.checkState(items.size() == 1);
+ Preconditions.checkState(items.get(0).getAction() == ItemAction.CANCEL);
+ items.clear();
+ }
+
+ /**
+ * Creates a new item.
+ * <p/>
+ * <ul>
+ * <li>In normal mode, we only consider ADD items. This happens when for instance an existing item was partially repaired
+ * and there is a need to create a new item which represents the part left -- that was not repaired.
+ * <li>In mergeMode, we allow to create new items that are the missing repaired items (CANCEL).
+ * </ul>
+ *
+ * @param startDate start date of the new item to create
+ * @param endDate end date of the new item to create
+ * @param mergeMode mode to consider.
+ * @return
+ */
+ private Item createNewItem(LocalDate startDate, LocalDate endDate, final boolean mergeMode) {
+
+ final Item item = getResultingItem(mergeMode);
+ if (item == null) {
+ return null;
+ }
+
+ final Item result = new Item(item.toProratedInvoiceItem(startDate, endDate), item.getAction());
+ if (item.getAction() == ItemAction.CANCEL && result != null) {
+ item.incrementCurrentRepairedAmount(result.getAmount());
+ }
+ return result;
+ }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
new file mode 100644
index 0000000..81b17ea
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tree;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.google.common.base.Preconditions;
+
+public class ItemsNodeInterval extends NodeInterval {
+
+ private ItemsInterval items;
+
+ public ItemsNodeInterval() {
+ this.items = new ItemsInterval(this);
+ }
+
+ public ItemsNodeInterval(final NodeInterval parent, final Item item) {
+ super(parent, item.getStartDate(), item.getEndDate());
+ this.items = new ItemsInterval(this, item);
+ }
+
+ @JsonIgnore
+ public ItemsInterval getItemsInterval() {
+ return items;
+ }
+
+ public List<Item> getItems() {
+ return items.getItems();
+ }
+
+ /**
+ * <p/>
+ * There is no limit in the depth of the tree,
+ * and the build strategy is to first consider the lowest child for a given period
+ * and go up the tree adding missing interval if needed. For e.g, one of the possible scenario:
+ * <pre>
+ * D1 D2
+ * |---------------------------------------------------| Plan P1
+ * D1' D2'
+ * |---------------|/////////////////////////////| Plan P2, REPAIR
+ *
+ * In that case we will generate:
+ * [D1,D1') on Plan P1; [D1', D2') on Plan P2, and [D2', D2) repair item
+ *
+ * <pre/>
+ *
+ * In the merge mode, the strategy is different, the tree is fairly shallow
+ * and the goal is to generate the repair items; @see addProposedItem
+ *
+ * @param output result list of built items
+ */
+ public void buildForExistingItems(final List<Item> output) {
+ build(new BuildNodeCallback() {
+ @Override
+ public void onMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+ final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
+ items.buildForMissingInterval(startDate, endDate, output, false);
+ }
+
+ @Override
+ public void onLastNode(final NodeInterval curNode) {
+ final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
+ items.buildFromItems(output, false);
+ }
+ });
+ }
+
+ /**
+ * The merge tree is initially constructed by flattening all the existing items and reversing them (CANCEL node).
+ * <p/>
+ * That means that if we were to not merge any new proposed items, we would end up with only those reversed existing
+ * items, and they would all end up repaired-- which is what we want.
+ * <p/>
+ * However, if there are new proposed items, then we look to see if they are children one our existing reverse items
+ * so that we can generate the repair pieces missing. For e.g, below is one scenario among so many:
+ * <p/>
+ * <pre>
+ * D1 D2
+ * |---------------------------------------------------| (existing reversed (CANCEL) item
+ * D1' D2'
+ * |---------------| (proposed same plan)
+ * </pre>
+ * In that case we want to generated a repair for [D1, D1') and [D2',D2)
+ * <p/>
+ * Note that this tree is never very deep, only 3 levels max, with exiting at the first level
+ * and proposed that are the for the exact same plan but for different dates below.
+ *
+ * @param output result list of built items
+ */
+ public void mergeExistingAndProposed(final List<Item> output) {
+ build(new BuildNodeCallback() {
+ @Override
+ public void onMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+ final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
+ items.buildForMissingInterval(startDate, endDate, output, true);
+ }
+
+ @Override
+ public void onLastNode(final NodeInterval curNode) {
+ final ItemsInterval items = ((ItemsNodeInterval) curNode).getItemsInterval();
+ items.buildFromItems(output, true);
+ }
+ });
+ }
+
+ /**
+ * Add existing item into the tree.
+ *
+ * @param newNode an existing item
+ */
+ public boolean addExistingItem(final ItemsNodeInterval newNode) {
+
+ return addNode(newNode, new AddNodeCallback() {
+ @Override
+ public boolean onExistingNode(final NodeInterval existingNode) {
+ if (!existingNode.isRoot() && newNode.getStart().compareTo(existingNode.getStart()) == 0 && newNode.getEnd().compareTo(existingNode.getEnd()) == 0) {
+ final Item item = newNode.getItems().get(0);
+ final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItemsInterval();
+ existingOrNewNodeItems.insertSortedItem(item);
+ }
+ // There is no new node added but instead we just populated the list of items for the already existing node.
+ return false;
+ }
+
+ @Override
+ public boolean shouldInsertNode(final NodeInterval insertionNode) {
+ // Always want to insert node in the tree when we find the right place.
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Add proposed item into the (flattened and reversed) tree.
+ *
+ * @param newNode a new proposed item
+ * @return true if the item was merged and will trigger a repair or false if the proposed item should be kept as such
+ * and no repair generated.
+ */
+ public boolean addProposedItem(final ItemsNodeInterval newNode) {
+
+ return addNode(newNode, new AddNodeCallback() {
+ @Override
+ public boolean onExistingNode(final NodeInterval existingNode) {
+ if (!shouldInsertNode(existingNode)) {
+ return false;
+ }
+
+ Preconditions.checkState(newNode.getStart().compareTo(existingNode.getStart()) == 0 && newNode.getEnd().compareTo(existingNode.getEnd()) == 0);
+ final Item item = newNode.getItems().get(0);
+ final ItemsInterval existingOrNewNodeItems = ((ItemsNodeInterval) existingNode).getItemsInterval();
+ existingOrNewNodeItems.cancelItems(item);
+ // In the merge logic, whether we really insert the node or find an existing node on which to insert items should be seen
+ // as an insertion (so as to avoid keeping that proposed item, see how return value of addProposedItem is used)
+ return true;
+ }
+
+ @Override
+ public boolean shouldInsertNode(final NodeInterval insertionNode) {
+ // The root level is solely for the reversed existing items. If there is a new node that does not fit below the level
+ // of reversed existing items, we want to return false and keep it outside of the tree. It should be 'kept as such'.
+ if (insertionNode.isRoot()) {
+ return false;
+ }
+
+ final ItemsInterval insertionNodeItems = ((ItemsNodeInterval) insertionNode).getItemsInterval();
+ Preconditions.checkState(insertionNodeItems.getItems().size() == 1, "Expected existing node to have only one item");
+ final Item insertionNodeItem = insertionNodeItems.getItems().get(0);
+ final Item newNodeItem = newNode.getItems().get(0);
+
+ // If we receive a new proposed that is the same kind as the reversed existing we want to insert it to generate
+ // a piece of repair
+ if (insertionNodeItem.isSameKind(newNodeItem)) {
+ return true;
+ // If not, then keep the proposed outside of the tree.
+ } else {
+ return false;
+ }
+ }
+ });
+ }
+
+ /**
+ * Add the adjustment amount on the item specified by the targetId.
+ *
+ * @param adjustementDate date of the adjustment
+ * @param amount amount of the adjustment
+ * @param targetId item that has been adjusted
+ */
+ public void addAdjustment(final LocalDate adjustementDate, final BigDecimal amount, final UUID targetId) {
+ // TODO we should really be using findNode(adjustementDate, new SearchCallback() instead but wrong dates in test
+ // creates test panic.
+ final NodeInterval node = findNode(new SearchCallback() {
+ @Override
+ public boolean isMatch(final NodeInterval curNode) {
+ return ((ItemsNodeInterval) curNode).getItemsInterval().containsItem(targetId);
+ }
+ });
+ Preconditions.checkNotNull(node, "Cannot add adjustement for item = " + targetId + ", date = " + adjustementDate);
+ ((ItemsNodeInterval) node).setAdjustment(amount.negate(), targetId);
+ }
+
+ public void jsonSerializeTree(final ObjectMapper mapper, final OutputStream output) throws IOException {
+
+ final JsonGenerator generator = mapper.getFactory().createJsonGenerator(output);
+ generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+
+ walkTree(new WalkCallback() {
+
+ private int curDepth = 0;
+
+ @Override
+ public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
+ final ItemsNodeInterval node = (ItemsNodeInterval) curNode;
+ if (node.isRoot()) {
+ return;
+ }
+
+ try {
+ if (curDepth < depth) {
+ generator.writeStartArray();
+ curDepth = depth;
+ } else if (curDepth > depth) {
+ generator.writeEndArray();
+ curDepth = depth;
+ }
+ generator.writeObject(node);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to deserialize tree", e);
+ }
+ }
+ });
+ generator.close();
+ }
+
+ protected void setAdjustment(final BigDecimal amount, final UUID linkedId) {
+ items.setAdjustment(amount, linkedId);
+ }
+
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/NodeInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/NodeInterval.java
new file mode 100644
index 0000000..05ff456
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/NodeInterval.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tree;
+
+import java.util.List;
+
+import org.joda.time.LocalDate;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+
+public class NodeInterval {
+
+ protected NodeInterval parent;
+ protected NodeInterval leftChild;
+ protected NodeInterval rightSibling;
+
+ protected LocalDate start;
+ protected LocalDate end;
+
+ public NodeInterval() {
+ this(null, null, null);
+ }
+
+ public NodeInterval(final NodeInterval parent, final LocalDate startDate, final LocalDate endDate) {
+ this.start = startDate;
+ this.end = endDate;
+ this.parent = parent;
+ this.leftChild = null;
+ this.rightSibling = null;
+ }
+
+ /**
+ * Build the tree by calling the callback on the last node in the tree or remaining part with no children.
+ *
+ * @param callback the callback which perform the build logic.
+ */
+ public void build(final BuildNodeCallback callback) {
+
+ Preconditions.checkNotNull(callback);
+
+ if (leftChild == null) {
+ callback.onLastNode(this);
+ return;
+ }
+
+ LocalDate curDate = start;
+ NodeInterval curChild = leftChild;
+ while (curChild != null) {
+ if (curChild.getStart().compareTo(curDate) > 0) {
+ callback.onMissingInterval(this, curDate, curChild.getStart());
+ }
+ curChild.build(callback);
+ curDate = curChild.getEnd();
+ curChild = curChild.getRightSibling();
+ }
+
+ // Finally if there is a hole at the end, we build the missing piece from ourself
+ if (curDate.compareTo(end) < 0) {
+ callback.onMissingInterval(this, curDate, end);
+ }
+ }
+
+ /**
+ * Add a new node in the tree.
+ *
+ * @param newNode the node to be added
+ * @param callback the callback that will allow to specify insertion and return behavior.
+ * @return true if node was inserted. Note that this is driven by the callback, this method is generic
+ * and specific behavior can be tuned through specific callbacks.
+ */
+ public boolean addNode(final NodeInterval newNode, final AddNodeCallback callback) {
+
+ Preconditions.checkNotNull(newNode);
+ Preconditions.checkNotNull(callback);
+
+ if (!isRoot() && newNode.getStart().compareTo(start) == 0 && newNode.getEnd().compareTo(end) == 0) {
+ return callback.onExistingNode(this);
+ }
+
+ computeRootInterval(newNode);
+
+ newNode.parent = this;
+ if (leftChild == null) {
+ if (callback.shouldInsertNode(this)) {
+ leftChild = newNode;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ NodeInterval prevChild = null;
+ NodeInterval curChild = leftChild;
+ while (curChild != null) {
+ if (curChild.isItemContained(newNode)) {
+ return curChild.addNode(newNode, callback);
+ }
+
+ if (curChild.isItemOverlap(newNode)) {
+ if (callback.shouldInsertNode(this)) {
+ rebalance(newNode);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ if (newNode.getStart().compareTo(curChild.getStart()) < 0) {
+ if (callback.shouldInsertNode(this)) {
+ newNode.rightSibling = curChild;
+ if (prevChild == null) {
+ leftChild = newNode;
+ } else {
+ prevChild.rightSibling = newNode;
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ prevChild = curChild;
+ curChild = curChild.rightSibling;
+ }
+
+ if (callback.shouldInsertNode(this)) {
+ prevChild.rightSibling = newNode;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Return the first node satisfying the date and match callback.
+ *
+ * @param targetDate target date for possible match nodes whose interval comprises that date
+ * @param callback custom logic to decide if a given node is a match
+ * @return the found node or null if there is nothing.
+ */
+ public NodeInterval findNode(final LocalDate targetDate, final SearchCallback callback) {
+
+ Preconditions.checkNotNull(callback);
+ Preconditions.checkNotNull(targetDate);
+
+ if (targetDate.compareTo(getStart()) < 0 || targetDate.compareTo(getEnd()) > 0) {
+ return null;
+ }
+
+ NodeInterval curChild = leftChild;
+ while (curChild != null) {
+ if (curChild.getStart().compareTo(targetDate) <= 0 && curChild.getEnd().compareTo(targetDate) >= 0) {
+ if (callback.isMatch(curChild)) {
+ return curChild;
+ }
+ NodeInterval result = curChild.findNode(targetDate, callback);
+ if (result != null) {
+ return result;
+ }
+ }
+ curChild = curChild.getRightSibling();
+ }
+ return null;
+ }
+
+ /**
+ * Return the first node satisfying the date and match callback.
+ *
+ * @param callback custom logic to decide if a given node is a match
+ * @return the found node or null if there is nothing.
+ */
+ public NodeInterval findNode(final SearchCallback callback) {
+
+ Preconditions.checkNotNull(callback);
+ if (callback.isMatch(this)) {
+ return this;
+ }
+
+ NodeInterval curChild = leftChild;
+ while (curChild != null) {
+ final NodeInterval result = curChild.findNode(callback);
+ if (result != null) {
+ return result;
+ }
+ curChild = curChild.getRightSibling();
+ }
+ return null;
+ }
+
+ /**
+ * Walk the tree (depth first search) and invoke callback for each node.
+ *
+ * @param callback
+ */
+ public void walkTree(WalkCallback callback) {
+ Preconditions.checkNotNull(callback);
+ walkTreeWithDepth(callback, 0);
+ }
+
+ private void walkTreeWithDepth(WalkCallback callback, int depth) {
+
+ Preconditions.checkNotNull(callback);
+ callback.onCurrentNode(depth, this, parent);
+
+ NodeInterval curChild = leftChild;
+ while (curChild != null) {
+ curChild.walkTreeWithDepth(callback, (depth + 1));
+ curChild = curChild.getRightSibling();
+ }
+ }
+
+
+ public boolean isItemContained(final NodeInterval newNode) {
+ return (newNode.getStart().compareTo(start) >= 0 &&
+ newNode.getStart().compareTo(end) <= 0 &&
+ newNode.getEnd().compareTo(start) >= 0 &&
+ newNode.getEnd().compareTo(end) <= 0);
+ }
+
+ public boolean isItemOverlap(final NodeInterval newNode) {
+ return ((newNode.getStart().compareTo(start) < 0 &&
+ newNode.getEnd().compareTo(end) >= 0) ||
+ (newNode.getStart().compareTo(start) <= 0 &&
+ newNode.getEnd().compareTo(end) > 0));
+ }
+
+ @JsonIgnore
+ public boolean isRoot() {
+ return parent == null;
+ }
+
+ public LocalDate getStart() {
+ return start;
+ }
+
+ public LocalDate getEnd() {
+ return end;
+ }
+
+ @JsonIgnore
+ public NodeInterval getParent() {
+ return parent;
+ }
+
+ @JsonIgnore
+ public NodeInterval getLeftChild() {
+ return leftChild;
+ }
+
+ @JsonIgnore
+ public NodeInterval getRightSibling() {
+ return rightSibling;
+ }
+
+ @JsonIgnore
+ public int getNbChildren() {
+ int result = 0;
+ NodeInterval curChild = leftChild;
+ while (curChild != null) {
+ result++;
+ curChild = curChild.rightSibling;
+ }
+ return result;
+ }
+
+ /**
+ * Since items may be added out of order, there is no guarantee that we don't suddenly have a new node
+ * whose interval emcompasses cuurent node(s). In which case we need to rebalance the tree.
+ *
+ * @param newNode node that triggered a rebalance operation
+ */
+ private void rebalance(final NodeInterval newNode) {
+
+ NodeInterval prevRebalanced = null;
+ NodeInterval curChild = leftChild;
+ List<NodeInterval> toBeRebalanced = Lists.newLinkedList();
+ do {
+ if (curChild.isItemOverlap(newNode)) {
+ toBeRebalanced.add(curChild);
+ } else {
+ if (toBeRebalanced.size() > 0) {
+ break;
+ }
+ prevRebalanced = curChild;
+ }
+ curChild = curChild.rightSibling;
+ } while (curChild != null);
+
+ newNode.parent = this;
+ final NodeInterval lastNodeToRebalance = toBeRebalanced.get(toBeRebalanced.size() - 1);
+ newNode.rightSibling = lastNodeToRebalance.rightSibling;
+ lastNodeToRebalance.rightSibling = null;
+ if (prevRebalanced == null) {
+ leftChild = newNode;
+ } else {
+ prevRebalanced.rightSibling = newNode;
+ }
+
+ NodeInterval prev = null;
+ for (NodeInterval cur : toBeRebalanced) {
+ cur.parent = newNode;
+ if (prev == null) {
+ newNode.leftChild = cur;
+ } else {
+ prev.rightSibling = cur;
+ }
+ prev = cur;
+ }
+ }
+
+ private void computeRootInterval(final NodeInterval newNode) {
+ if (!isRoot()) {
+ return;
+ }
+ this.start = (start == null || start.compareTo(newNode.getStart()) > 0) ? newNode.getStart() : start;
+ this.end = (end == null || end.compareTo(newNode.getEnd()) < 0) ? newNode.getEnd() : end;
+ }
+
+ /**
+ * Provides callback for walking the tree.
+ */
+ public interface WalkCallback {
+ public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent);
+ }
+
+ /**
+ * Provides custom logic for the search.
+ */
+ public interface SearchCallback {
+ /**
+ * Custom logic to decide which node to return.
+ *
+ * @param curNode found node
+ * @return evaluates whether this is the node that should be returned
+ */
+ boolean isMatch(NodeInterval curNode);
+ }
+
+ /**
+ * Provides the custom logic for when building resulting state from the tree.
+ */
+ public interface BuildNodeCallback {
+
+ /**
+ * Called when we hit a missing interval where there is no child.
+ *
+ * @param curNode current node
+ * @param startDate startDate of the new interval to build
+ * @param endDate endDate of the new interval to build
+ */
+ public void onMissingInterval(NodeInterval curNode, LocalDate startDate, LocalDate endDate);
+
+ /**
+ * Called when we hit a node with no children
+ *
+ * @param curNode current node
+ */
+ public void onLastNode(NodeInterval curNode);
+ }
+
+ /**
+ * Provides the custom logic for when adding nodes in the tree.
+ */
+ public interface AddNodeCallback {
+
+ /**
+ * Called when trying to insert a new node in the tree but there is already
+ * such a node for that same interval.
+ *
+ * @param existingNode
+ * @return this is the return value for the addNode method
+ */
+ public boolean onExistingNode(final NodeInterval existingNode);
+
+ /**
+ * Called prior to insert the new node in the tree
+ *
+ * @param insertionNode the parent node where this new node would be inserted
+ * @return true if addNode should proceed with the insertion and false otherwise
+ */
+ public boolean shouldInsertNode(final NodeInterval insertionNode);
+ }
+}
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
new file mode 100644
index 0000000..5079499
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tree;
+
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.tree.Item.ItemAction;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+
+/**
+ * Tree of invoice items for a given subscription.
+ */
+public class SubscriptionItemTree {
+
+ private boolean isBuilt;
+
+ private final UUID subscriptionId;
+ private ItemsNodeInterval root;
+
+ private List<Item> items;
+
+ private List<InvoiceItem> existingFixedItems;
+ private List<InvoiceItem> remainingFixedItems;
+ private List<InvoiceItem> pendingItemAdj;
+
+ private static final Comparator<InvoiceItem> INVOICE_ITEM_COMPARATOR = new Comparator<InvoiceItem>() {
+ @Override
+ public int compare(final InvoiceItem o1, final InvoiceItem o2) {
+ int startDateComp = o1.getStartDate().compareTo(o2.getStartDate());
+ if (startDateComp != 0) {
+ return startDateComp;
+ }
+ int itemTypeComp = (o1.getInvoiceItemType().ordinal()<o2.getInvoiceItemType().ordinal() ? -1 :
+ (o1.getInvoiceItemType().ordinal()==o2.getInvoiceItemType().ordinal() ? 0 : 1));
+ if (itemTypeComp != 0) {
+ return itemTypeComp;
+ }
+ Preconditions.checkState(false, "Unexpected list of items for subscription " + o1.getSubscriptionId());
+ // Never reached...
+ return 0;
+ }
+ };
+
+ public SubscriptionItemTree(final UUID subscriptionId) {
+ this.subscriptionId = subscriptionId;
+ this.root = new ItemsNodeInterval();
+ this.items = new LinkedList<Item>();
+ this.existingFixedItems = new LinkedList<InvoiceItem>();
+ this.remainingFixedItems = new LinkedList<InvoiceItem>();
+ this.pendingItemAdj = new LinkedList<InvoiceItem>();
+ this.isBuilt = false;
+ }
+
+ /**
+ * Build the tree to return the list of existing items.
+ */
+ public void build() {
+ Preconditions.checkState(!isBuilt);
+ for (InvoiceItem item : pendingItemAdj) {
+ root.addAdjustment(item.getStartDate(), item.getAmount(), item.getLinkedItemId());
+ }
+ pendingItemAdj.clear();
+ root.buildForExistingItems(items);
+ isBuilt = true;
+ }
+
+ /**
+ * Flattens the tree so its depth only has one level below root -- becomes a list.
+ * <p>
+ * If the tree was not built, it is first built. The list of items is cleared and the state is now reset to unbuilt.
+ *
+ * @param reverse whether to reverse the existing items (recurring items now show up as CANCEL instead of ADD)
+ */
+ public void flatten(boolean reverse) {
+ if (!isBuilt) {
+ build();
+ }
+ root = new ItemsNodeInterval();
+ for (Item item : items) {
+ Preconditions.checkState(item.getAction() == ItemAction.ADD);
+ root.addExistingItem(new ItemsNodeInterval(root, new Item(item, reverse ? ItemAction.CANCEL : ItemAction.ADD)));
+ }
+ items.clear();
+ isBuilt = false;
+ }
+
+ public void buildForMerge() {
+ Preconditions.checkState(!isBuilt);
+ root.mergeExistingAndProposed(items);
+ isBuilt = true;
+ }
+
+ /**
+ * Add an existing item in the tree.
+ *
+ * @param invoiceItem new existing invoice item on disk.
+ */
+ public void addItem(final InvoiceItem invoiceItem) {
+
+ Preconditions.checkState(!isBuilt);
+ switch (invoiceItem.getInvoiceItemType()) {
+ case RECURRING:
+ root.addExistingItem(new ItemsNodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
+ break;
+
+ case REPAIR_ADJ:
+ root.addExistingItem(new ItemsNodeInterval(root, new Item(invoiceItem, ItemAction.CANCEL)));
+ break;
+
+ case FIXED:
+ existingFixedItems.add(invoiceItem);
+ break;
+
+ case ITEM_ADJ:
+ pendingItemAdj.add(invoiceItem);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Merge a new proposed ietm in the tree.
+ *
+ * @param invoiceItem new proposed item that should be merged in the existing tree
+ */
+ public void mergeProposedItem(final InvoiceItem invoiceItem) {
+
+ Preconditions.checkState(!isBuilt);
+ switch (invoiceItem.getInvoiceItemType()) {
+ case RECURRING:
+ final boolean result = root.addProposedItem(new ItemsNodeInterval(root, new Item(invoiceItem, ItemAction.ADD)));
+ if (!result) {
+ items.add(new Item(invoiceItem, ItemAction.ADD));
+ }
+ 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.add(invoiceItem);
+ }
+ break;
+
+ default:
+ Preconditions.checkState(false, "Unexpected proposed item " + invoiceItem);
+ }
+
+ }
+
+ /**
+ * Can be called prior or after merge with proposed items.
+ * <ul>
+ * <li>When called prior, the merge this gives a flat view of the existing items on disk
+ * <li>When called after the merge with proposed items, this gives the list of items that should now be written to disk -- new fixed, recurring and repair.
+ * </ul>
+ * @return a flat view of the items in the tree.
+ */
+ public List<InvoiceItem> getView() {
+
+ final List<InvoiceItem> tmp = new LinkedList<InvoiceItem>();
+ tmp.addAll(remainingFixedItems);
+ tmp.addAll(Collections2.filter(Collections2.transform(items, new Function<Item, InvoiceItem>() {
+ @Override
+ public InvoiceItem apply(final Item input) {
+ return input.toInvoiceItem();
+ }
+ }), new Predicate<InvoiceItem>() {
+ @Override
+ public boolean apply(@Nullable final InvoiceItem input) {
+ return input != null;
+ }
+ }));
+
+ final List<InvoiceItem> result = Ordering.<InvoiceItem>from(INVOICE_ITEM_COMPARATOR).sortedCopy(tmp);
+ checkItemsListState(result);
+ return result;
+ }
+
+ // Verify there is no double billing, and no double repair (credits)
+ private void checkItemsListState(final List<InvoiceItem> orderedList) {
+
+ LocalDate prevRecurringEndDate = null;
+ LocalDate prevRepairEndDate = null;
+ for (InvoiceItem cur : orderedList) {
+ switch (cur.getInvoiceItemType()) {
+ case FIXED:
+ break;
+
+ case RECURRING:
+ if (prevRecurringEndDate != null) {
+ Preconditions.checkState(prevRecurringEndDate.compareTo(cur.getStartDate()) <= 0);
+ }
+ prevRecurringEndDate = cur.getEndDate();
+ break;
+
+ case REPAIR_ADJ:
+ if (prevRepairEndDate != null) {
+ Preconditions.checkState(prevRepairEndDate.compareTo(cur.getStartDate()) <= 0);
+ }
+ prevRepairEndDate = cur.getEndDate();
+ break;
+
+ default:
+ Preconditions.checkState(false, "Unexpected item type " + cur.getInvoiceItemType());
+ }
+ }
+ }
+
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SubscriptionItemTree)) {
+ return false;
+ }
+
+ final SubscriptionItemTree that = (SubscriptionItemTree) o;
+
+ if (root != null ? !root.equals(that.root) : that.root != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = subscriptionId != null ? subscriptionId.hashCode() : 0;
+ result = 31 * result + (root != null ? root.hashCode() : 0);
+ return result;
+ }
+
+ @VisibleForTesting
+ ItemsNodeInterval getRoot() {
+ return root;
+ }
+}
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
new file mode 100644
index 0000000..f5d7eb2
--- /dev/null
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
@@ -0,0 +1,56 @@
+group InvoiceItemSqlDao: EntitySqlDao;
+
+tableName() ::= "invoice_items"
+
+tableFields(prefix) ::= <<
+ <prefix>type
+, <prefix>invoice_id
+, <prefix>account_id
+, <prefix>bundle_id
+, <prefix>subscription_id
+, <prefix>plan_name
+, <prefix>phase_name
+, <prefix>start_date
+, <prefix>end_date
+, <prefix>amount
+, <prefix>rate
+, <prefix>currency
+, <prefix>linked_item_id
+, <prefix>created_by
+, <prefix>created_date
+>>
+
+tableValues() ::= <<
+ :type
+, :invoiceId
+, :accountId
+, :bundleId
+, :subscriptionId
+, :planName
+, :phaseName
+, :startDate
+, :endDate
+, :amount
+, :rate
+, :currency
+, :linkedItemId
+, :createdBy
+, :createdDate
+>>
+
+
+getInvoiceItemsByInvoice() ::= <<
+ SELECT <allTableFields()>
+ FROM <tableName()>
+ WHERE invoice_id = :invoiceId
+ <AND_CHECK_TENANT()>
+ ;
+>>
+
+getInvoiceItemsBySubscription() ::= <<
+ SELECT <allTableFields()>
+ FROM <tableName()>
+ WHERE subscription_id = :subscriptionId
+ <AND_CHECK_TENANT()>
+ ;
+>>
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
new file mode 100644
index 0000000..8d1383d
--- /dev/null
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg
@@ -0,0 +1,101 @@
+group InvoicePayment: EntitySqlDao;
+
+tableName() ::= "invoice_payments"
+
+tableFields(prefix) ::= <<
+ <prefix>type
+, <prefix>invoice_id
+, <prefix>payment_id
+, <prefix>payment_date
+, <prefix>amount
+, <prefix>currency
+, <prefix>processed_currency
+, <prefix>payment_cookie_id
+, <prefix>linked_invoice_payment_id
+, <prefix>created_by
+, <prefix>created_date
+>>
+
+tableValues() ::= <<
+ :type
+, :invoiceId
+, :paymentId
+, :paymentDate
+, :amount
+, :currency
+, :processedCurrency
+, :paymentCookieId
+, :linkedInvoicePaymentId
+, :createdBy
+, :createdDate
+>>
+
+getByPaymentId() ::= <<
+ SELECT <allTableFields()>
+ FROM <tableName()>
+ WHERE payment_id = :paymentId
+ <AND_CHECK_TENANT()>
+ ;
+>>
+
+getPaymentsForCookieId() ::= <<
+ SELECT <allTableFields()>
+ FROM <tableName()>
+ WHERE payment_cookie_id = :paymentCookieId
+ <AND_CHECK_TENANT()>
+ ;
+>>
+
+getPaymentsForInvoice() ::= <<
+ SELECT <allTableFields()>
+ FROM <tableName()>
+ WHERE invoice_id = :invoiceId
+ <AND_CHECK_TENANT()>
+ ;
+>>
+
+getInvoicePayments() ::= <<
+ SELECT <allTableFields()>
+ FROM <tableName()>
+ WHERE payment_id = :paymentId
+ <AND_CHECK_TENANT()>
+ ;
+>>
+
+getRemainingAmountPaid() ::= <<
+ SELECT SUM(amount)
+ FROM <tableName()>
+ WHERE (id = :invoicePaymentId OR linked_invoice_payment_id = :invoicePaymentId)
+ <AND_CHECK_TENANT()>
+ ;
+>>
+
+getAccountIdFromInvoicePaymentId() ::= <<
+ SELECT i.account_id
+ FROM <tableName()> ip
+ INNER JOIN invoices i ON i.id = ip.invoice_id
+ WHERE ip.id = :invoicePaymentId
+ <AND_CHECK_TENANT("i.")>
+ <AND_CHECK_TENANT("ip.")>
+ ;
+>>
+
+getChargeBacksByAccountId() ::= <<
+ SELECT <allTableFields("ip.")>
+ FROM <tableName()> ip
+ INNER JOIN invoices i ON i.id = ip.invoice_id
+ WHERE ip.type = 'CHARGED_BACK' AND i.account_id = :accountId
+ <AND_CHECK_TENANT("i.")>
+ <AND_CHECK_TENANT("ip.")>
+ ;
+>>
+
+getChargebacksByPaymentId() ::= <<
+ SELECT <allTableFields()>
+ FROM <tableName()>
+ WHERE type = 'CHARGED_BACK'
+ AND linked_invoice_payment_id IN (SELECT id FROM invoice_payments WHERE payment_id = :paymentId)
+ <AND_CHECK_TENANT()>
+ ;
+>>
+
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
new file mode 100644
index 0000000..8daa8b1
--- /dev/null
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceSqlDao.sql.stg
@@ -0,0 +1,52 @@
+group InvoiceDao: EntitySqlDao;
+
+tableName() ::= "invoices"
+
+tableFields(prefix) ::= <<
+ <prefix>account_id
+, <prefix>invoice_date
+, <prefix>target_date
+, <prefix>currency
+, <prefix>migrated
+, <prefix>created_by
+, <prefix>created_date
+>>
+
+tableValues() ::= <<
+ :accountId
+, :invoiceDate
+, :targetDate
+, :currency
+, :migrated
+, :createdBy
+, :createdDate
+>>
+
+extraTableFieldsWithComma(prefix) ::= <<
+, <prefix>record_id as invoice_number
+>>
+
+getInvoicesBySubscription() ::= <<
+ SELECT <allTableFields("i.")>
+ FROM <tableName()> i
+ JOIN invoice_items ii ON i.id = ii.invoice_id
+ WHERE ii.subscription_id = :subscriptionId AND i.migrated = '0'
+ <AND_CHECK_TENANT("i.")>
+ <AND_CHECK_TENANT("ii.")>
+ ;
+>>
+
+searchQuery(prefix) ::= <<
+ <idField(prefix)> = :searchKey
+ or <prefix>account_id = :searchKey
+ or <prefix>currency = :searchKey
+>>
+
+getInvoiceIdByPaymentId() ::= <<
+ SELECT i.id
+ FROM <tableName()> i, invoice_payments ip
+ WHERE ip.invoice_id = i.id
+ AND ip.payment_id = :paymentId
+ <AND_CHECK_TENANT("i.")>
+ <AND_CHECK_TENANT("ip.")>
+>>
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql b/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
new file mode 100644
index 0000000..beb05f1
--- /dev/null
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
@@ -0,0 +1,74 @@
+/*! SET storage_engine=INNODB */;
+
+DROP TABLE IF EXISTS invoice_items;
+CREATE TABLE invoice_items (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ type varchar(24) NOT NULL,
+ invoice_id char(36) NOT NULL,
+ account_id char(36) NOT NULL,
+ bundle_id char(36),
+ subscription_id char(36),
+ plan_name varchar(50),
+ phase_name varchar(50),
+ start_date date NOT NULL,
+ end_date date,
+ amount numeric(15,9) NOT NULL,
+ rate numeric(15,9) NULL,
+ currency char(3) NOT NULL,
+ linked_item_id char(36),
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX invoice_items_id ON invoice_items(id);
+CREATE INDEX invoice_items_subscription_id ON invoice_items(subscription_id ASC);
+CREATE INDEX invoice_items_invoice_id ON invoice_items(invoice_id ASC);
+CREATE INDEX invoice_items_account_id ON invoice_items(account_id ASC);
+CREATE INDEX invoice_items_tenant_account_record_id ON invoice_items(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS invoices;
+CREATE TABLE invoices (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ account_id char(36) NOT NULL,
+ invoice_date date NOT NULL,
+ target_date date NOT NULL,
+ currency char(3) NOT NULL,
+ migrated bool NOT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX invoices_id ON invoices(id);
+CREATE INDEX invoices_account_target ON invoices(account_id ASC, target_date);
+CREATE INDEX invoices_tenant_account_record_id ON invoices(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS invoice_payments;
+CREATE TABLE invoice_payments (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ type varchar(24) NOT NULL,
+ invoice_id char(36) NOT NULL,
+ payment_id char(36),
+ payment_date datetime NOT NULL,
+ amount numeric(15,9) NOT NULL,
+ currency char(3) NOT NULL,
+ processed_currency char(3) NOT NULL,
+ payment_cookie_id char(36) DEFAULT NULL,
+ linked_invoice_payment_id char(36) DEFAULT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX invoice_payments_id ON invoice_payments(id);
+CREATE INDEX invoice_payments ON invoice_payments(payment_id);
+CREATE INDEX invoice_payments_invoice_id ON invoice_payments(invoice_id);
+CREATE INDEX invoice_payments_reversals ON invoice_payments(linked_invoice_payment_id);
+CREATE INDEX invoice_payments_tenant_account_record_id ON invoice_payments(tenant_record_id, account_record_id);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
new file mode 100644
index 0000000..bef93eb
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/invoice/TestDefaultInvoicePaymentApi.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.invoice;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import static org.killbill.billing.invoice.tests.InvoiceTestUtils.createAndPersistInvoice;
+import static org.killbill.billing.invoice.tests.InvoiceTestUtils.createAndPersistPayment;
+
+public class TestDefaultInvoicePaymentApi extends InvoiceTestSuiteWithEmbeddedDB {
+
+ private static final BigDecimal THIRTY = new BigDecimal("30.00");
+ private static final Currency CURRENCY = Currency.EUR;
+
+ @Test(groups = "slow")
+ public void testFullRefundWithNoAdjustment() throws Exception {
+ verifyRefund(THIRTY, THIRTY, THIRTY, false, ImmutableMap.<UUID, BigDecimal>of());
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundWithNoAdjustment() throws Exception {
+ verifyRefund(THIRTY, BigDecimal.TEN, BigDecimal.TEN, false, ImmutableMap.<UUID, BigDecimal>of());
+ }
+
+ @Test(groups = "slow")
+ public void testFullRefundWithInvoiceAdjustment() throws Exception {
+ verifyRefund(THIRTY, THIRTY, BigDecimal.ZERO, true, ImmutableMap.<UUID, BigDecimal>of());
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundWithInvoiceAdjustment() throws Exception {
+ verifyRefund(THIRTY, BigDecimal.TEN, BigDecimal.ZERO, true, ImmutableMap.<UUID, BigDecimal>of());
+ }
+
+ @Test(groups = "slow")
+ public void testFullRefundWithBothInvoiceItemAdjustments() throws Exception {
+ // Create an invoice with two items (30 \u20ac and 10 \u20ac)
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock,
+ ImmutableList.<BigDecimal>of(THIRTY, BigDecimal.TEN), CURRENCY, internalCallContext);
+
+ // Fully adjust both items
+ final Map<UUID, BigDecimal> adjustments = new HashMap<UUID, BigDecimal>();
+ adjustments.put(invoice.getInvoiceItems().get(0).getId(), null);
+ adjustments.put(invoice.getInvoiceItems().get(1).getId(), null);
+
+ verifyRefund(invoice, new BigDecimal("40"), new BigDecimal("40"), BigDecimal.ZERO, true, adjustments);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundWithSingleInvoiceItemAdjustment() throws Exception {
+ // Create an invoice with two items (30 \u20ac and 10 \u20ac)
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock,
+ ImmutableList.<BigDecimal>of(THIRTY, BigDecimal.TEN), CURRENCY, internalCallContext);
+
+ // Fully adjust both items
+ final Map<UUID, BigDecimal> adjustments = new HashMap<UUID, BigDecimal>();
+ adjustments.put(invoice.getInvoiceItems().get(0).getId(), null);
+
+ verifyRefund(invoice, new BigDecimal("40"), new BigDecimal("30"), BigDecimal.ZERO, true, adjustments);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialRefundWithTwoInvoiceItemAdjustment() throws Exception {
+ // Create an invoice with two items (30 \u20ac and 10 \u20ac)
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock,
+ ImmutableList.<BigDecimal>of(THIRTY, BigDecimal.TEN), CURRENCY, internalCallContext);
+ // Adjust partially both items: the invoice posted was 40 \u20ac, but we should really just have charged you 2 \u20ac
+ final ImmutableMap<UUID, BigDecimal> adjustments = ImmutableMap.<UUID, BigDecimal>of(invoice.getInvoiceItems().get(0).getId(), new BigDecimal("29"),
+ invoice.getInvoiceItems().get(1).getId(), new BigDecimal("9"));
+ verifyRefund(invoice, new BigDecimal("40"), new BigDecimal("38"), BigDecimal.ZERO, true, adjustments);
+ }
+
+ private void verifyRefund(final BigDecimal invoiceAmount, final BigDecimal refundAmount, final BigDecimal finalInvoiceAmount,
+ final boolean adjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts) throws InvoiceApiException {
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock, invoiceAmount, CURRENCY, internalCallContext);
+ verifyRefund(invoice, invoiceAmount, refundAmount, finalInvoiceAmount, adjusted, invoiceItemIdsWithAmounts);
+ }
+
+ private void verifyRefund(final Invoice invoice, final BigDecimal invoiceAmount, final BigDecimal refundAmount, final BigDecimal finalInvoiceAmount,
+ final boolean adjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts) throws InvoiceApiException {
+ final InvoicePayment payment = createAndPersistPayment(invoiceInternalApi, clock, invoice.getId(), invoiceAmount, CURRENCY, internalCallContext);
+
+ // Verify the initial invoice balance
+ final BigDecimal initialInvoiceBalance = invoicePaymentApi.getInvoice(invoice.getId(), callContext).getBalance();
+ Assert.assertEquals(initialInvoiceBalance.compareTo(BigDecimal.ZERO), 0);
+
+ // Create a full refund with no adjustment
+ final InvoicePayment refund = invoiceInternalApi.createRefund(payment.getPaymentId(), refundAmount, adjusted, invoiceItemIdsWithAmounts,
+ UUID.randomUUID(), internalCallContext);
+ Assert.assertEquals(refund.getAmount().compareTo(refundAmount.negate()), 0);
+ Assert.assertEquals(refund.getCurrency(), CURRENCY);
+ Assert.assertEquals(refund.getInvoiceId(), invoice.getId());
+ Assert.assertEquals(refund.getPaymentId(), payment.getPaymentId());
+ Assert.assertEquals(refund.getType(), InvoicePaymentType.REFUND);
+
+ // Verify the current invoice balance
+ final BigDecimal newInvoiceBalance = invoicePaymentApi.getInvoice(invoice.getId(), callContext).getBalance();
+ Assert.assertEquals(newInvoiceBalance.compareTo(finalInvoiceAmount), 0);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java
new file mode 100644
index 0000000..066a99c
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/migration/TestDefaultInvoiceMigrationApi.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.migration;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.dao.InvoiceModelDao;
+import org.killbill.billing.invoice.dao.InvoiceModelDaoHelper;
+
+public class TestDefaultInvoiceMigrationApi extends InvoiceTestSuiteWithEmbeddedDB {
+
+ private final Logger log = LoggerFactory.getLogger(TestDefaultInvoiceMigrationApi.class);
+
+ private LocalDate date_migrated;
+ private DateTime date_regular;
+
+ private UUID accountId;
+ private UUID migrationInvoiceId;
+ private UUID regularInvoiceId;
+
+ private static final BigDecimal MIGRATION_INVOICE_AMOUNT = new BigDecimal("100.00");
+ private static final Currency MIGRATION_INVOICE_CURRENCY = Currency.USD;
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ date_migrated = clock.getUTCToday().minusYears(1);
+ date_regular = clock.getUTCNow();
+
+ final Account account = invoiceUtil.createAccount(callContext);
+ accountId = account.getId();
+ migrationInvoiceId = createAndCheckMigrationInvoice(accountId);
+ regularInvoiceId = invoiceUtil.generateRegularInvoice(account, date_regular, callContext);
+ }
+
+ private UUID createAndCheckMigrationInvoice(final UUID accountId) throws InvoiceApiException {
+ final UUID migrationInvoiceId = migrationApi.createMigrationInvoice(accountId, date_migrated, MIGRATION_INVOICE_AMOUNT,
+ MIGRATION_INVOICE_CURRENCY, callContext);
+ Assert.assertNotNull(migrationInvoiceId);
+ //Double check it was created and values are correct
+
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, callContext);
+ final InvoiceModelDao invoice = invoiceDao.getById(migrationInvoiceId, internalTenantContext);
+ Assert.assertNotNull(invoice);
+
+ Assert.assertEquals(invoice.getAccountId(), accountId);
+ Assert.assertEquals(invoice.getTargetDate().compareTo(date_migrated), 0); //temp to avoid tz test artifact
+ // Assert.assertEquals(invoice.getTargetDate(),now);
+ Assert.assertEquals(invoice.getInvoiceItems().size(), 1);
+ Assert.assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(MIGRATION_INVOICE_AMOUNT), 0);
+ Assert.assertEquals(InvoiceModelDaoHelper.getBalance(invoice).compareTo(MIGRATION_INVOICE_AMOUNT), 0);
+ Assert.assertEquals(invoice.getCurrency(), MIGRATION_INVOICE_CURRENCY);
+ Assert.assertTrue(invoice.isMigrated());
+
+ return migrationInvoiceId;
+ }
+
+ @Test(groups = "slow")
+ public void testUserApiAccess() {
+ final List<Invoice> byAccount = invoiceUserApi.getInvoicesByAccount(accountId, callContext);
+ Assert.assertEquals(byAccount.size(), 1);
+ Assert.assertEquals(byAccount.get(0).getId(), regularInvoiceId);
+
+ final List<Invoice> byAccountAndDate = invoiceUserApi.getInvoicesByAccount(accountId, date_migrated.minusDays(1), callContext);
+ Assert.assertEquals(byAccountAndDate.size(), 1);
+ Assert.assertEquals(byAccountAndDate.get(0).getId(), regularInvoiceId);
+
+ final Collection<Invoice> unpaid = invoiceUserApi.getUnpaidInvoicesByAccountId(accountId, new LocalDate(date_regular.plusDays(1)), callContext);
+ Assert.assertEquals(unpaid.size(), 2);
+ }
+
+ // Check migration invoice IS returned for payment api calls
+ @Test(groups = "slow")
+ public void testPaymentApi() {
+ final List<Invoice> allByAccount = invoicePaymentApi.getAllInvoicesByAccount(accountId, callContext);
+ Assert.assertEquals(allByAccount.size(), 2);
+ Assert.assertTrue(checkContains(allByAccount, regularInvoiceId));
+ Assert.assertTrue(checkContains(allByAccount, migrationInvoiceId));
+ }
+
+ // ACCOUNT balance should reflect total of migration and non-migration invoices
+ @Test(groups = "slow")
+ public void testBalance() throws InvoiceApiException {
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, callContext);
+ final InvoiceModelDao migrationInvoice = invoiceDao.getById(migrationInvoiceId, internalTenantContext);
+ final InvoiceModelDao regularInvoice = invoiceDao.getById(regularInvoiceId, internalTenantContext);
+ final BigDecimal balanceOfAllInvoices = InvoiceModelDaoHelper.getBalance(migrationInvoice).add(InvoiceModelDaoHelper.getBalance(regularInvoice));
+
+ final BigDecimal accountBalance = invoiceUserApi.getAccountBalance(accountId, callContext);
+ log.info("ACCOUNT balance: " + accountBalance + " should equal the Balance Of All Invoices: " + balanceOfAllInvoices);
+ Assert.assertEquals(accountBalance.compareTo(balanceOfAllInvoices), 0);
+ }
+
+ private boolean checkContains(final List<Invoice> invoices, final UUID invoiceId) {
+ for (final Invoice invoice : invoices) {
+ if (invoice.getId().equals(invoiceId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/MockInvoicePaymentApi.java b/invoice/src/test/java/org/killbill/billing/invoice/api/MockInvoicePaymentApi.java
new file mode 100644
index 0000000..d284698
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/MockInvoicePaymentApi.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.model.DefaultInvoicePayment;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+public class MockInvoicePaymentApi implements InvoicePaymentApi {
+
+ private final CopyOnWriteArrayList<Invoice> invoices = new CopyOnWriteArrayList<Invoice>();
+ private final CopyOnWriteArrayList<InvoicePayment> invoicePayments = new CopyOnWriteArrayList<InvoicePayment>();
+
+ public void add(final Invoice invoice) {
+ invoices.add(invoice);
+ }
+
+ @Override
+ public List<Invoice> getAllInvoicesByAccount(final UUID accountId, final TenantContext context) {
+ final ArrayList<Invoice> result = new ArrayList<Invoice>();
+
+ for (final Invoice invoice : invoices) {
+ if (accountId.equals(invoice.getAccountId())) {
+ result.add(invoice);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Invoice getInvoice(final UUID invoiceId, final TenantContext context) {
+ for (final Invoice invoice : invoices) {
+ if (invoiceId.equals(invoice.getId())) {
+ return invoice;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public List<InvoicePayment> getInvoicePayments(final UUID paymentId, final TenantContext context) {
+ final List<InvoicePayment> result = new LinkedList<InvoicePayment>();
+ for (final InvoicePayment invoicePayment : invoicePayments) {
+ if (paymentId.equals(invoicePayment.getPaymentId())) {
+ result.add(invoicePayment);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public InvoicePayment getInvoicePaymentForAttempt(final UUID paymentId, final TenantContext context) {
+ for (final InvoicePayment invoicePayment : invoicePayments) {
+ if (paymentId.equals(invoicePayment.getPaymentId()) && invoicePayment.getType() == InvoicePaymentType.ATTEMPT) {
+ return invoicePayment;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public InvoicePayment createChargeback(final UUID invoicePaymentId, final BigDecimal amount, final CallContext context) throws InvoiceApiException {
+ InvoicePayment existingPayment = null;
+ for (final InvoicePayment payment : invoicePayments) {
+ if (payment.getId() == invoicePaymentId) {
+ existingPayment = payment;
+ break;
+ }
+ }
+
+ if (existingPayment != null) {
+ invoicePayments.add(new DefaultInvoicePayment(UUID.randomUUID(), InvoicePaymentType.CHARGED_BACK, null, null, DateTime.now(DateTimeZone.UTC), amount,
+ existingPayment.getCurrency(), existingPayment.getProcessedCurrency(), null, existingPayment.getId()));
+ }
+
+ return existingPayment;
+ }
+
+ @Override
+ public InvoicePayment createChargeback(final UUID invoicePaymentId, final CallContext context) throws InvoiceApiException {
+ InvoicePayment existingPayment = null;
+ for (final InvoicePayment payment : invoicePayments) {
+ if (payment.getId() == invoicePaymentId) {
+ existingPayment = payment;
+ }
+ }
+
+ if (existingPayment != null) {
+ this.createChargeback(invoicePaymentId, existingPayment.getAmount(), context);
+ }
+
+ return existingPayment;
+ }
+
+ @Override
+ public BigDecimal getRemainingAmountPaid(final UUID invoicePaymentId, final TenantContext context) {
+ BigDecimal amount = BigDecimal.ZERO;
+ for (final InvoicePayment payment : invoicePayments) {
+ if (payment.getId().equals(invoicePaymentId)) {
+ amount = amount.add(payment.getAmount());
+ }
+
+ if (payment.getLinkedInvoicePaymentId().equals(invoicePaymentId)) {
+ amount = amount.add(payment.getAmount());
+ }
+ }
+
+ return amount;
+ }
+
+ @Override
+ public List<InvoicePayment> getChargebacksByAccountId(final UUID accountId, final TenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public UUID getAccountIdFromInvoicePaymentId(final UUID uuid, final TenantContext context) throws InvoiceApiException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<InvoicePayment> getChargebacksByPaymentId(final UUID paymentId, final TenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InvoicePayment getChargebackById(final UUID chargebackId, final TenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestEventJson.java b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestEventJson.java
new file mode 100644
index 0000000..6fa5710
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/api/user/TestEventJson.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.user;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+import org.killbill.billing.events.InvoiceCreationInternalEvent;
+import org.killbill.billing.events.NullInvoiceInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+public class TestEventJson extends InvoiceTestSuiteNoDB {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ @Test(groups = "fast")
+ public void testInvoiceCreationEvent() throws Exception {
+ final InvoiceCreationInternalEvent e = new DefaultInvoiceCreationEvent(UUID.randomUUID(), UUID.randomUUID(), new BigDecimal(12.0), Currency.USD, 1L, 2L, null);
+ final String json = mapper.writeValueAsString(e);
+
+ final Object obj = mapper.readValue(json, DefaultInvoiceCreationEvent.class);
+ Assert.assertEquals(obj, e);
+ }
+
+ @Test(groups = "fast")
+ public void testEmptyInvoiceEvent() throws Exception {
+ final NullInvoiceInternalEvent e = new DefaultNullInvoiceEvent(UUID.randomUUID(), new LocalDate(), 1L, 2L, null);
+ final String json = mapper.writeValueAsString(e);
+
+ final Object obj = mapper.readValue(json, DefaultNullInvoiceEvent.class);
+ Assert.assertEquals(obj, e);
+ }
+}
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
new file mode 100644
index 0000000..879fe02
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.user.DefaultInvoiceCreationEvent;
+import org.killbill.billing.util.entity.DefaultPagination;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.MockEntityDaoBase;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.inject.Inject;
+
+public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice, InvoiceApiException> implements InvoiceDao {
+
+ private final PersistentBus eventBus;
+ private final Object monitor = new Object();
+ private final Map<UUID, InvoiceModelDao> invoices = new LinkedHashMap<UUID, InvoiceModelDao>();
+ private final Map<UUID, InvoiceItemModelDao> items = new LinkedHashMap<UUID, InvoiceItemModelDao>();
+ private final Map<UUID, InvoicePaymentModelDao> payments = new LinkedHashMap<UUID, InvoicePaymentModelDao>();
+ private final BiMap<UUID, Long> accountRecordIds = HashBiMap.create();
+
+ @Inject
+ public MockInvoiceDao(final PersistentBus eventBus) {
+ this.eventBus = eventBus;
+ }
+
+ @Override
+ public void createInvoice(final InvoiceModelDao invoice, final List<InvoiceItemModelDao> invoiceItems,
+ final List<InvoicePaymentModelDao> invoicePayments, final boolean isRealInvoice, final Map<UUID, DateTime> callbackDateTimePerSubscriptions, final InternalCallContext context) {
+ synchronized (monitor) {
+ invoices.put(invoice.getId(), invoice);
+ for (final InvoiceItemModelDao invoiceItemModelDao : invoiceItems) {
+ items.put(invoiceItemModelDao.getId(), invoiceItemModelDao);
+ }
+ for (final InvoicePaymentModelDao paymentModelDao : invoicePayments) {
+ payments.put(paymentModelDao.getId(), paymentModelDao);
+ }
+ accountRecordIds.put(invoice.getAccountId(), context.getAccountRecordId());
+ }
+ try {
+ eventBus.post(new DefaultInvoiceCreationEvent(invoice.getId(), invoice.getAccountId(),
+ InvoiceModelDaoHelper.getBalance(invoice), invoice.getCurrency(),
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()));
+ } catch (PersistentBus.EventBusException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ public InvoiceModelDao getById(final UUID id, final InternalTenantContext context) {
+ synchronized (monitor) {
+ return invoices.get(id);
+ }
+ }
+
+ @Override
+ public InvoiceModelDao getByNumber(final Integer number, final InternalTenantContext context) {
+ synchronized (monitor) {
+ for (final InvoiceModelDao invoice : invoices.values()) {
+ if (invoice.getInvoiceNumber().equals(number)) {
+ return invoice;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public Pagination<InvoiceModelDao> getAll(final InternalTenantContext context) {
+ synchronized (monitor) {
+ return new DefaultPagination<InvoiceModelDao>((long) invoices.values().size(), invoices.values().iterator());
+ }
+ }
+
+ @Override
+ public List<InvoiceModelDao> getInvoicesByAccount(final InternalTenantContext context) {
+ final List<InvoiceModelDao> result = new ArrayList<InvoiceModelDao>();
+
+ synchronized (monitor) {
+ final UUID accountId = accountRecordIds.inverse().get(context.getAccountRecordId());
+ for (final InvoiceModelDao invoice : invoices.values()) {
+ if (accountId.equals(invoice.getAccountId()) && !invoice.isMigrated()) {
+ result.add(invoice);
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public List<InvoiceModelDao> getInvoicesByAccount(final LocalDate fromDate, final InternalTenantContext context) {
+ final List<InvoiceModelDao> invoicesForAccount = new ArrayList<InvoiceModelDao>();
+
+ synchronized (monitor) {
+ final UUID accountId = accountRecordIds.inverse().get(context.getAccountRecordId());
+ for (final InvoiceModelDao invoice : getAll(context)) {
+ if (accountId.equals(invoice.getAccountId()) && !invoice.getTargetDate().isBefore(fromDate) && !invoice.isMigrated()) {
+ invoicesForAccount.add(invoice);
+ }
+ }
+ }
+
+ return invoicesForAccount;
+ }
+
+ @Override
+ public List<InvoiceModelDao> getInvoicesBySubscription(final UUID subscriptionId, final InternalTenantContext context) {
+ final List<InvoiceModelDao> result = new ArrayList<InvoiceModelDao>();
+
+ synchronized (monitor) {
+ for (final InvoiceModelDao invoice : invoices.values()) {
+ for (final InvoiceItemModelDao item : invoice.getInvoiceItems()) {
+ if (subscriptionId.equals(item.getSubscriptionId()) && !invoice.isMigrated()) {
+ result.add(invoice);
+ break;
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Pagination<InvoiceModelDao> searchInvoices(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+ final List<InvoiceModelDao> results = new LinkedList<InvoiceModelDao>();
+ for (final InvoiceModelDao invoice : getAll(context)) {
+ if (invoice.getId().toString().equals(searchKey) ||
+ invoice.getAccountId().toString().equals(searchKey) ||
+ invoice.getInvoiceNumber().toString().equals(searchKey) ||
+ invoice.getCurrency().toString().equals(searchKey)) {
+ results.add(invoice);
+ }
+ }
+
+ return DefaultPagination.<InvoiceModelDao>build(offset, limit, results);
+ }
+
+ @Override
+ public void test(final InternalTenantContext context) {
+ }
+
+ @Override
+ public UUID getInvoiceIdByPaymentId(final UUID paymentId, final InternalTenantContext context) {
+ synchronized (monitor) {
+ for (final InvoicePaymentModelDao payment : payments.values()) {
+ if (paymentId.equals(payment.getPaymentId())) {
+ return payment.getInvoiceId();
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public List<InvoicePaymentModelDao> getInvoicePayments(final UUID paymentId, final InternalTenantContext context) {
+ final List<InvoicePaymentModelDao> result = new LinkedList<InvoicePaymentModelDao>();
+ synchronized (monitor) {
+ for (final InvoicePaymentModelDao payment : payments.values()) {
+ if (paymentId.equals(payment.getPaymentId())) {
+ result.add(payment);
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public void notifyOfPayment(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
+ synchronized (monitor) {
+ payments.put(invoicePayment.getId(), invoicePayment);
+ }
+ }
+
+ @Override
+ public void consumeExstingCBAOnAccountWithUnpaidInvoices(final UUID accountId, final InternalCallContext context) {
+ }
+
+ @Override
+ public BigDecimal getAccountBalance(final UUID accountId, final InternalTenantContext context) {
+ BigDecimal balance = BigDecimal.ZERO;
+
+ for (final InvoiceModelDao invoice : getAll(context)) {
+ if (accountId.equals(invoice.getAccountId())) {
+ balance = balance.add(InvoiceModelDaoHelper.getBalance(invoice));
+ }
+ }
+
+ return balance;
+ }
+
+ @Override
+ public List<InvoiceModelDao> getUnpaidInvoicesByAccountId(final UUID accountId, final LocalDate upToDate, final InternalTenantContext context) {
+ final List<InvoiceModelDao> unpaidInvoices = new ArrayList<InvoiceModelDao>();
+
+ for (final InvoiceModelDao invoice : getAll(context)) {
+ if (accountId.equals(invoice.getAccountId()) && (InvoiceModelDaoHelper.getBalance(invoice).compareTo(BigDecimal.ZERO) > 0) && !invoice.isMigrated()) {
+ unpaidInvoices.add(invoice);
+ }
+ }
+
+ return unpaidInvoices;
+ }
+
+ @Override
+ public List<InvoiceModelDao> getAllInvoicesByAccount(final InternalTenantContext context) {
+ final List<InvoiceModelDao> result = new ArrayList<InvoiceModelDao>();
+
+ synchronized (monitor) {
+ final UUID accountId = accountRecordIds.inverse().get(context.getAccountRecordId());
+ for (final InvoiceModelDao invoice : invoices.values()) {
+ if (accountId.equals(invoice.getAccountId())) {
+ result.add(invoice);
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public InvoicePaymentModelDao postChargeback(final UUID invoicePaymentId, final BigDecimal amount, final InternalCallContext context) throws InvoiceApiException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BigDecimal getRemainingAmountPaid(final UUID invoicePaymentId, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public UUID getAccountIdFromInvoicePaymentId(final UUID invoicePaymentId, final InternalTenantContext context) throws InvoiceApiException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<InvoicePaymentModelDao> getChargebacksByAccountId(final UUID accountId, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<InvoicePaymentModelDao> getChargebacksByPaymentId(final UUID paymentId, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InvoicePaymentModelDao getChargebackById(final UUID chargebackId, final InternalTenantContext context) throws InvoiceApiException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InvoiceItemModelDao getExternalChargeById(final UUID externalChargeId, final InternalTenantContext context) throws InvoiceApiException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InvoiceItemModelDao insertExternalCharge(final UUID accountId, @Nullable final UUID invoiceId, @Nullable final UUID bundleId,
+ @Nullable final String description, final BigDecimal amount, final LocalDate effectiveDate,
+ final Currency currency, final InternalCallContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InvoiceItemModelDao getCreditById(final UUID creditId, final InternalTenantContext context) throws InvoiceApiException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InvoiceItemModelDao insertCredit(final UUID accountId, final UUID invoiceId, final BigDecimal amount, final LocalDate effectiveDate,
+ final Currency currency, final InternalCallContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InvoiceItemModelDao insertInvoiceItemAdjustment(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId,
+ final LocalDate effectiveDate, @Nullable final BigDecimal amount,
+ @Nullable final Currency currency, final InternalCallContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BigDecimal getAccountCBA(final UUID accountId, final InternalTenantContext context) {
+ return null;
+ }
+
+ @Override
+ public InvoicePaymentModelDao createRefund(final UUID paymentId, final BigDecimal amount, final boolean isInvoiceAdjusted,
+ final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final UUID paymentCookieId,
+ final InternalCallContext context)
+ throws InvoiceApiException {
+ return null;
+ }
+
+ @Override
+ public void deleteCBA(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId, final InternalCallContext context) throws InvoiceApiException {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestDefaultInvoiceDaoUnit.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestDefaultInvoiceDaoUnit.java
new file mode 100644
index 0000000..052ac21
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestDefaultInvoiceDaoUnit.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.math.BigDecimal;
+import java.util.Map;
+import java.util.UUID;
+
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+
+import com.google.common.collect.ImmutableMap;
+
+public class TestDefaultInvoiceDaoUnit extends InvoiceTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testComputePositiveRefundAmount() throws Exception {
+ // Verify the cases with no adjustment first
+ final Map<UUID, BigDecimal> noItemAdjustment = ImmutableMap.<UUID, BigDecimal>of();
+ verifyComputedRefundAmount(null, null, noItemAdjustment, BigDecimal.ZERO);
+ verifyComputedRefundAmount(null, BigDecimal.ZERO, noItemAdjustment, BigDecimal.ZERO);
+ verifyComputedRefundAmount(BigDecimal.TEN, null, noItemAdjustment, BigDecimal.TEN);
+ verifyComputedRefundAmount(BigDecimal.TEN, BigDecimal.ONE, noItemAdjustment, BigDecimal.ONE);
+ try {
+ verifyComputedRefundAmount(BigDecimal.ONE, BigDecimal.TEN, noItemAdjustment, BigDecimal.TEN);
+ Assert.fail("Shouldn't have been able to compute a refund amount");
+ } catch (InvoiceApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.REFUND_AMOUNT_TOO_HIGH.getCode());
+ }
+
+ // Try with adjustments now
+ final Map<UUID, BigDecimal> itemAdjustments = ImmutableMap.<UUID, BigDecimal>of(UUID.randomUUID(), BigDecimal.ONE,
+ UUID.randomUUID(), BigDecimal.TEN,
+ UUID.randomUUID(), BigDecimal.ZERO);
+ verifyComputedRefundAmount(new BigDecimal("100"), new BigDecimal("11"), itemAdjustments, new BigDecimal("11"));
+ try {
+ verifyComputedRefundAmount(new BigDecimal("100"), BigDecimal.TEN, itemAdjustments, BigDecimal.TEN);
+ Assert.fail("Shouldn't have been able to compute a refund amount");
+ } catch (InvoiceApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.REFUND_AMOUNT_DONT_MATCH_ITEMS_TO_ADJUST.getCode());
+ }
+ }
+
+ private void verifyComputedRefundAmount(final BigDecimal paymentAmount, final BigDecimal requestedAmount,
+ final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final BigDecimal expectedRefundAmount) throws InvoiceApiException {
+ final InvoicePaymentModelDao invoicePayment = Mockito.mock(InvoicePaymentModelDao.class);
+ Mockito.when(invoicePayment.getAmount()).thenReturn(paymentAmount);
+
+ final InvoiceDaoHelper invoiceDaoHelper = new InvoiceDaoHelper();
+ final BigDecimal actualRefundAmount = invoiceDaoHelper.computePositiveRefundAmount(invoicePayment, requestedAmount, invoiceItemIdsWithAmounts);
+ Assert.assertEquals(actualRefundAmount, expectedRefundAmount);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java
new file mode 100644
index 0000000..51189b1
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.model.DefaultInvoice;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+
+public class TestInvoiceDaoForItemAdjustment extends InvoiceTestSuiteWithEmbeddedDB {
+
+ private static final BigDecimal INVOICE_ITEM_AMOUNT = new BigDecimal("21.00");
+
+ @Test(groups = "slow")
+ public void testAddInvoiceItemAdjustmentForNonExistingInvoiceItemId() throws Exception {
+ final UUID accountId = UUID.randomUUID();
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID invoiceItemId = UUID.randomUUID();
+ final LocalDate effectiveDate = new LocalDate();
+
+ try {
+ invoiceDao.insertInvoiceItemAdjustment(accountId, invoiceId, invoiceItemId, effectiveDate, null, null, internalCallContext);
+ Assert.fail("Should not have been able to adjust a non existing invoice item");
+ } catch (Exception e) {
+ Assert.assertEquals(((InvoiceApiException) e.getCause()).getCode(), ErrorCode.INVOICE_ITEM_NOT_FOUND.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testAddInvoiceItemAdjustmentForWrongInvoice() throws Exception {
+ final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoice.getId(), invoice.getAccountId(), UUID.randomUUID(),
+ UUID.randomUUID(), "test plan", "test phase",
+ new LocalDate(2010, 1, 1), new LocalDate(2010, 4, 1),
+ INVOICE_ITEM_AMOUNT, new BigDecimal("7.00"), Currency.USD);
+ invoice.addInvoiceItem(invoiceItem);
+ invoiceUtil.createInvoice(invoice, true, internalCallContext);
+
+ try {
+ invoiceDao.insertInvoiceItemAdjustment(invoice.getAccountId(), UUID.randomUUID(), invoiceItem.getId(), new LocalDate(2010, 1, 1), null, null, internalCallContext);
+ Assert.fail("Should not have been able to adjust an item on a non existing invoice");
+ } catch (Exception e) {
+ Assert.assertEquals(((InvoiceApiException) e.getCause()).getCode(), ErrorCode.INVOICE_INVALID_FOR_INVOICE_ITEM_ADJUSTMENT.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testAddInvoiceItemAdjustmentForFullAmount() throws Exception {
+ final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoice.getId(), invoice.getAccountId(), UUID.randomUUID(),
+ UUID.randomUUID(), "test plan", "test phase",
+ new LocalDate(2010, 1, 1), new LocalDate(2010, 4, 1),
+ INVOICE_ITEM_AMOUNT, new BigDecimal("7.00"), Currency.USD);
+ invoice.addInvoiceItem(invoiceItem);
+ invoiceUtil.createInvoice(invoice, true, internalCallContext);
+
+ final InvoiceItemModelDao adjustedInvoiceItem = createAndCheckAdjustment(invoice, invoiceItem, null);
+ Assert.assertEquals(adjustedInvoiceItem.getAmount().compareTo(invoiceItem.getAmount().negate()), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testAddInvoiceItemAdjustmentForPartialAmount() throws Exception {
+ final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceItem invoiceItem = new RecurringInvoiceItem(invoice.getId(), invoice.getAccountId(), UUID.randomUUID(),
+ UUID.randomUUID(), "test plan", "test phase",
+ new LocalDate(2010, 1, 1), new LocalDate(2010, 4, 1),
+ INVOICE_ITEM_AMOUNT, new BigDecimal("7.00"), Currency.USD);
+ invoice.addInvoiceItem(invoiceItem);
+ invoiceUtil.createInvoice(invoice, true, internalCallContext);
+
+ final InvoiceItemModelDao adjustedInvoiceItem = createAndCheckAdjustment(invoice, invoiceItem, BigDecimal.TEN);
+ Assert.assertEquals(adjustedInvoiceItem.getAmount().compareTo(BigDecimal.TEN.negate()), 0);
+ }
+
+ private InvoiceItemModelDao createAndCheckAdjustment(final Invoice invoice, final InvoiceItem invoiceItem, final BigDecimal amount) throws InvoiceApiException {
+ final LocalDate effectiveDate = new LocalDate(2010, 1, 1);
+ final InvoiceItemModelDao adjustedInvoiceItem = invoiceDao.insertInvoiceItemAdjustment(invoice.getAccountId(), invoice.getId(), invoiceItem.getId(),
+ effectiveDate, amount, null, internalCallContext);
+ Assert.assertEquals(adjustedInvoiceItem.getAccountId(), invoiceItem.getAccountId());
+ Assert.assertNull(adjustedInvoiceItem.getBundleId());
+ Assert.assertEquals(adjustedInvoiceItem.getCurrency(), invoiceItem.getCurrency());
+ Assert.assertEquals(adjustedInvoiceItem.getEndDate(), effectiveDate);
+ Assert.assertEquals(adjustedInvoiceItem.getInvoiceId(), invoiceItem.getInvoiceId());
+ Assert.assertEquals(adjustedInvoiceItem.getType(), InvoiceItemType.ITEM_ADJ);
+ Assert.assertEquals(adjustedInvoiceItem.getLinkedItemId(), invoiceItem.getId());
+ Assert.assertNull(adjustedInvoiceItem.getPhaseName());
+ Assert.assertNull(adjustedInvoiceItem.getPlanName());
+ Assert.assertNull(adjustedInvoiceItem.getRate());
+ Assert.assertEquals(adjustedInvoiceItem.getStartDate(), effectiveDate);
+ Assert.assertNull(adjustedInvoiceItem.getSubscriptionId());
+
+ // Retrieve the item by id
+ final InvoiceItemModelDao retrievedInvoiceItem = invoiceUtil.getInvoiceItemById(adjustedInvoiceItem.getId(), internalCallContext);
+ // TODO We can't use equals() due to the createdDate field
+ Assert.assertEquals(retrievedInvoiceItem.getAccountId(), adjustedInvoiceItem.getAccountId());
+ Assert.assertNull(retrievedInvoiceItem.getBundleId());
+ Assert.assertEquals(retrievedInvoiceItem.getCurrency(), adjustedInvoiceItem.getCurrency());
+ Assert.assertEquals(retrievedInvoiceItem.getEndDate(), adjustedInvoiceItem.getEndDate());
+ Assert.assertEquals(retrievedInvoiceItem.getInvoiceId(), adjustedInvoiceItem.getInvoiceId());
+ Assert.assertEquals(retrievedInvoiceItem.getType(), adjustedInvoiceItem.getType());
+ Assert.assertEquals(retrievedInvoiceItem.getLinkedItemId(), adjustedInvoiceItem.getLinkedItemId());
+ Assert.assertNull(retrievedInvoiceItem.getPhaseName());
+ Assert.assertNull(retrievedInvoiceItem.getPlanName());
+ Assert.assertNull(retrievedInvoiceItem.getRate());
+ Assert.assertEquals(retrievedInvoiceItem.getStartDate(), adjustedInvoiceItem.getStartDate());
+ Assert.assertNull(retrievedInvoiceItem.getSubscriptionId());
+
+ // Retrieve the item by invoice id
+ final InvoiceModelDao retrievedInvoice = invoiceDao.getById(adjustedInvoiceItem.getInvoiceId(), internalCallContext);
+ final List<InvoiceItemModelDao> invoiceItems = retrievedInvoice.getInvoiceItems();
+ Assert.assertEquals(invoiceItems.size(), 2);
+ final InvoiceItemModelDao retrievedByInvoiceInvoiceItem;
+ if (invoiceItems.get(0).getId().equals(adjustedInvoiceItem.getId())) {
+ retrievedByInvoiceInvoiceItem = invoiceItems.get(0);
+ } else {
+ retrievedByInvoiceInvoiceItem = invoiceItems.get(1);
+ }
+ // TODO We can't use equals() due to the createdDate field
+ Assert.assertEquals(retrievedByInvoiceInvoiceItem.getAccountId(), adjustedInvoiceItem.getAccountId());
+ Assert.assertEquals(retrievedByInvoiceInvoiceItem.getBundleId(), adjustedInvoiceItem.getBundleId());
+ Assert.assertEquals(retrievedByInvoiceInvoiceItem.getCurrency(), adjustedInvoiceItem.getCurrency());
+ Assert.assertEquals(retrievedByInvoiceInvoiceItem.getEndDate(), adjustedInvoiceItem.getEndDate());
+ Assert.assertEquals(retrievedByInvoiceInvoiceItem.getInvoiceId(), adjustedInvoiceItem.getInvoiceId());
+ Assert.assertEquals(retrievedByInvoiceInvoiceItem.getType(), adjustedInvoiceItem.getType());
+ Assert.assertEquals(retrievedByInvoiceInvoiceItem.getLinkedItemId(), adjustedInvoiceItem.getLinkedItemId());
+ Assert.assertEquals(retrievedByInvoiceInvoiceItem.getPhaseName(), adjustedInvoiceItem.getPhaseName());
+ Assert.assertEquals(retrievedByInvoiceInvoiceItem.getPlanName(), adjustedInvoiceItem.getPlanName());
+ Assert.assertEquals(retrievedByInvoiceInvoiceItem.getRate(), adjustedInvoiceItem.getRate());
+ Assert.assertEquals(retrievedInvoiceItem.getStartDate(), adjustedInvoiceItem.getStartDate());
+ Assert.assertEquals(retrievedInvoiceItem.getSubscriptionId(), adjustedInvoiceItem.getSubscriptionId());
+
+ // Verify the invoice balance
+ if (amount == null) {
+ Assert.assertEquals(InvoiceModelDaoHelper.getBalance(retrievedInvoice).compareTo(BigDecimal.ZERO), 0);
+ } else {
+ Assert.assertEquals(InvoiceModelDaoHelper.getBalance(retrievedInvoice).compareTo(INVOICE_ITEM_AMOUNT.add(amount.negate())), 0);
+ }
+
+ return adjustedInvoiceItem;
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceItemDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceItemDao.java
new file mode 100644
index 0000000..99e777e
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceItemDao.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.dao;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.entity.EntityPersistenceException;
+import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
+import org.killbill.billing.invoice.model.DefaultInvoice;
+import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
+import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.invoice.model.InvoiceItemFactory;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.TEN;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestInvoiceItemDao extends InvoiceTestSuiteWithEmbeddedDB {
+
+ private Account account;
+ private InternalCallContext context;
+
+ @BeforeMethod(groups = "slow")
+ public void setUp() throws Exception {
+ account = invoiceUtil.createAccount(callContext);
+ context = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+ }
+
+ @Test(groups = "slow")
+ public void testInvoiceItemCreation() throws EntityPersistenceException {
+ final UUID accountId = account.getId();
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final UUID subscriptionId = UUID.randomUUID();
+ final LocalDate startDate = new LocalDate(2011, 10, 1);
+ final LocalDate endDate = new LocalDate(2011, 11, 1);
+ final BigDecimal rate = new BigDecimal("20.00");
+
+ final RecurringInvoiceItem item = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, "test plan", "test phase", startDate, endDate,
+ rate, rate, Currency.USD);
+ invoiceUtil.createInvoiceItem(item, context);
+
+ final InvoiceItemModelDao thisItem = invoiceUtil.getInvoiceItemById(item.getId(), context);
+ assertNotNull(thisItem);
+ assertEquals(thisItem.getId(), item.getId());
+ assertEquals(thisItem.getInvoiceId(), item.getInvoiceId());
+ assertEquals(thisItem.getSubscriptionId(), item.getSubscriptionId());
+ assertTrue(thisItem.getStartDate().compareTo(item.getStartDate()) == 0);
+ assertTrue(thisItem.getEndDate().compareTo(item.getEndDate()) == 0);
+ assertEquals(thisItem.getAmount().compareTo(item.getRate()), 0);
+ assertEquals(thisItem.getRate().compareTo(item.getRate()), 0);
+ assertEquals(thisItem.getCurrency(), item.getCurrency());
+ // created date is no longer set before persistence layer call
+ // assertEquals(thisItem.getCreatedDate().compareTo(item.getCreatedDate()), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testGetInvoiceItemsBySubscriptionId() throws EntityPersistenceException {
+ final UUID accountId = account.getId();
+ final UUID subscriptionId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final LocalDate startDate = new LocalDate(2011, 3, 1);
+ final BigDecimal rate = new BigDecimal("20.00");
+
+ for (int i = 0; i < 3; i++) {
+ final UUID invoiceId = UUID.randomUUID();
+
+ final RecurringInvoiceItem item = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId,
+ "test plan", "test phase", startDate.plusMonths(i), startDate.plusMonths(i + 1),
+ rate, rate, Currency.USD);
+ invoiceUtil.createInvoiceItem(item, context);
+ }
+
+ final List<InvoiceItemModelDao> items = invoiceUtil.getInvoiceItemBySubscriptionId(subscriptionId, context);
+ assertEquals(items.size(), 3);
+ }
+
+ @Test(groups = "slow")
+ public void testGetInvoiceItemsByInvoiceId() throws EntityPersistenceException {
+ final UUID accountId = account.getId();
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final LocalDate startDate = new LocalDate(2011, 3, 1);
+ final BigDecimal rate = new BigDecimal("20.00");
+
+ for (int i = 0; i < 5; i++) {
+ final UUID subscriptionId = UUID.randomUUID();
+ final BigDecimal amount = rate.multiply(new BigDecimal(i + 1));
+
+ final RecurringInvoiceItem item = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId,
+ "test plan", "test phase", startDate, startDate.plusMonths(1),
+ amount, amount, Currency.USD);
+ invoiceUtil.createInvoiceItem(item, context);
+ }
+
+ final List<InvoiceItemModelDao> items = invoiceUtil.getInvoiceItemByInvoiceId(invoiceId, context);
+ assertEquals(items.size(), 5);
+ }
+
+ @Test(groups = "slow")
+ public void testGetInvoiceItemsByAccountId() throws EntityPersistenceException {
+ final UUID accountId = account.getId();
+ final UUID bundleId = UUID.randomUUID();
+ final LocalDate targetDate = new LocalDate(2011, 5, 23);
+ final DefaultInvoice invoice = new DefaultInvoice(accountId, clock.getUTCToday(), targetDate, Currency.USD);
+
+ invoiceUtil.createInvoice(invoice, true, context);
+
+ final UUID invoiceId = invoice.getId();
+ final LocalDate startDate = new LocalDate(2011, 3, 1);
+ final BigDecimal rate = new BigDecimal("20.00");
+
+ final UUID subscriptionId = UUID.randomUUID();
+
+ final RecurringInvoiceItem item = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId,
+ "test plan", "test phase", startDate, startDate.plusMonths(1),
+ rate, rate, Currency.USD);
+ invoiceUtil.createInvoiceItem(item, context);
+
+ final List<InvoiceItemModelDao> items = invoiceUtil.getInvoiceItemByAccountId(context);
+ assertEquals(items.size(), 1);
+ }
+
+ @Test(groups = "slow")
+ public void testCreditBalanceInvoiceSqlDao() throws EntityPersistenceException {
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID accountId = account.getId();
+ final LocalDate creditDate = new LocalDate(2012, 4, 1);
+
+ final InvoiceItem creditInvoiceItem = new CreditBalanceAdjInvoiceItem(invoiceId, accountId, creditDate, TEN, Currency.USD);
+ invoiceUtil.createInvoiceItem(creditInvoiceItem, context);
+
+ final InvoiceItemModelDao savedItem = invoiceUtil.getInvoiceItemById(creditInvoiceItem.getId(), context);
+ assertSameInvoiceItem(creditInvoiceItem, savedItem);
+ }
+
+ @Test(groups = "slow")
+ public void testFixedPriceInvoiceSqlDao() throws EntityPersistenceException {
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID accountId = account.getId();
+ final LocalDate startDate = new LocalDate(2012, 4, 1);
+
+ final InvoiceItem fixedPriceInvoiceItem = new FixedPriceInvoiceItem(invoiceId, accountId, UUID.randomUUID(),
+ UUID.randomUUID(), "test plan", "test phase", startDate, TEN, Currency.USD);
+ invoiceUtil.createInvoiceItem(fixedPriceInvoiceItem, context);
+
+ final InvoiceItemModelDao savedItem = invoiceUtil.getInvoiceItemById(fixedPriceInvoiceItem.getId(), context);
+ assertSameInvoiceItem(fixedPriceInvoiceItem, savedItem);
+ }
+
+ @Test(groups = "slow")
+ public void testExternalChargeInvoiceSqlDao() throws Exception {
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID accountId = account.getId();
+ final UUID bundleId = UUID.randomUUID();
+ final String description = UUID.randomUUID().toString();
+ final LocalDate startDate = new LocalDate(2012, 4, 1);
+ final InvoiceItem externalChargeInvoiceItem = new ExternalChargeInvoiceItem(invoiceId, accountId, bundleId, description,
+ startDate, TEN, Currency.USD);
+ invoiceUtil.createInvoiceItem(externalChargeInvoiceItem, context);
+
+ final InvoiceItemModelDao savedItem = invoiceUtil.getInvoiceItemById(externalChargeInvoiceItem.getId(), context);
+ assertSameInvoiceItem(externalChargeInvoiceItem, savedItem);
+ }
+
+ @Test(groups = "slow")
+ public void testExternalChargeForVariousCurrenciesInvoiceSqlDao() throws Exception {
+ // 1 decimal place
+ createAndVerifyExternalCharge(new BigDecimal("10"), Currency.VND);
+ createAndVerifyExternalCharge(new BigDecimal("10.1"), Currency.VND);
+ // 2 decimal places
+ createAndVerifyExternalCharge(new BigDecimal("10"), Currency.USD);
+ createAndVerifyExternalCharge(new BigDecimal("10.1"), Currency.USD);
+ createAndVerifyExternalCharge(new BigDecimal("10.01"), Currency.USD);
+ // 3 decimal places
+ createAndVerifyExternalCharge(new BigDecimal("10"), Currency.BHD);
+ createAndVerifyExternalCharge(new BigDecimal("10.1"), Currency.BHD);
+ createAndVerifyExternalCharge(new BigDecimal("10.01"), Currency.BHD);
+ createAndVerifyExternalCharge(new BigDecimal("10.001"), Currency.BHD);
+ // 8 decimal places
+ createAndVerifyExternalCharge(new BigDecimal("10"), Currency.BTC);
+ createAndVerifyExternalCharge(new BigDecimal("10.1"), Currency.BTC);
+ createAndVerifyExternalCharge(new BigDecimal("10.01"), Currency.BTC);
+ createAndVerifyExternalCharge(new BigDecimal("10.001"), Currency.BTC);
+ createAndVerifyExternalCharge(new BigDecimal("10.0001"), Currency.BTC);
+ createAndVerifyExternalCharge(new BigDecimal("10.00001"), Currency.BTC);
+ createAndVerifyExternalCharge(new BigDecimal("10.000001"), Currency.BTC);
+ createAndVerifyExternalCharge(new BigDecimal("10.0000001"), Currency.BTC);
+ createAndVerifyExternalCharge(new BigDecimal("10.00000001"), Currency.BTC);
+ }
+
+ private void createAndVerifyExternalCharge(final BigDecimal amount, final Currency currency) throws EntityPersistenceException {
+ final InvoiceItem externalChargeInvoiceItem = new ExternalChargeInvoiceItem(UUID.randomUUID(), account.getId(), UUID.randomUUID(),
+ UUID.randomUUID().toString(), new LocalDate(2012, 4, 1), amount, currency);
+ invoiceUtil.createInvoiceItem(externalChargeInvoiceItem, context);
+
+ final InvoiceItemModelDao savedItem = invoiceUtil.getInvoiceItemById(externalChargeInvoiceItem.getId(), context);
+ assertSameInvoiceItem(externalChargeInvoiceItem, savedItem);
+ Assert.assertEquals(externalChargeInvoiceItem.getAmount().compareTo(amount), 0);
+ }
+
+ private void assertSameInvoiceItem(final InvoiceItem initialItem, final InvoiceItemModelDao fromDao) {
+ final InvoiceItem newItem = InvoiceItemFactory.fromModelDao(fromDao);
+ Assert.assertEquals(newItem.getId(), initialItem.getId());
+ Assert.assertTrue(newItem.matches(initialItem));
+
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestBillingIntervalDetail.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestBillingIntervalDetail.java
new file mode 100644
index 0000000..9941eb9
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestBillingIntervalDetail.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.generator;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+
+public class TestBillingIntervalDetail extends InvoiceTestSuiteNoDB {
+
+ /*
+ *
+ * Start BCD END_MONTH
+ * |---------|------------|-------|
+ *
+ */
+ @Test(groups = "fast")
+ public void testCalculateFirstBillingCycleDate1() throws Exception {
+ final LocalDate from = new LocalDate("2012-01-16");
+ final int bcd = 17;
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL);
+ billingIntervalDetail.calculateFirstBillingCycleDate();
+ Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-01-17"));
+ }
+
+ /*
+ *
+ * Start END_MONTH BCD
+ * |---------|-------------------| - - - -|
+ *
+ */
+ @Test(groups = "fast")
+ public void testCalculateFirstBillingCycleDate2() throws Exception {
+ final LocalDate from = new LocalDate("2012-02-16");
+ final int bcd = 30;
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL);
+ billingIntervalDetail.calculateFirstBillingCycleDate();
+ Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-02-29"));
+ }
+
+ /*
+ * Here the interesting part is that BCD is prior start and
+ * i) we use MONTHLY billing period
+ * ii) on the next month, there is no such date (2012-02-30 does not exist)
+ *
+ * Start
+ * BCD END_MONTH
+ * |----------------------------|--------|
+ *
+ */
+ @Test(groups = "fast")
+ public void testCalculateFirstBillingCycleDate4() throws Exception {
+ final LocalDate from = new LocalDate("2012-01-31");
+ final int bcd = 30;
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.MONTHLY);
+ billingIntervalDetail.calculateFirstBillingCycleDate();
+ Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2012-02-29"));
+ }
+
+ /*
+ *
+ * BCD Start END_MONTH
+ * |---------|-------------------|-----------|
+ *
+ */
+ @Test(groups = "fast")
+ public void testCalculateFirstBillingCycleDate3() throws Exception {
+ final LocalDate from = new LocalDate("2012-02-16");
+ final int bcd = 14;
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), bcd, BillingPeriod.ANNUAL);
+ billingIntervalDetail.calculateFirstBillingCycleDate();
+ Assert.assertEquals(billingIntervalDetail.getFirstBillingCycleDate(), new LocalDate("2013-02-14"));
+ }
+
+ @Test(groups = "fast")
+ public void testNextBCDShouldNotBeInThePast() throws Exception {
+ final LocalDate from = new LocalDate("2012-07-16");
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 15, BillingPeriod.MONTHLY);
+ final LocalDate to = billingIntervalDetail.getFirstBillingCycleDate();
+ Assert.assertEquals(to, new LocalDate("2012-08-15"));
+ }
+
+ @Test(groups = "fast")
+ public void testBeforeBCDWithOnOrAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-02");
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 3, BillingPeriod.MONTHLY);
+ final LocalDate to = billingIntervalDetail.getFirstBillingCycleDate();
+ Assert.assertEquals(to, new LocalDate("2012-03-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testEqualBCDWithOnOrAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-03");
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 3, BillingPeriod.MONTHLY);
+ final LocalDate to = billingIntervalDetail.getFirstBillingCycleDate();
+ Assert.assertEquals(to, new LocalDate("2012-03-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testAfterBCDWithOnOrAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-04");
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(from, null, new LocalDate(), 3, BillingPeriod.MONTHLY);
+ final LocalDate to = billingIntervalDetail.getFirstBillingCycleDate();
+ Assert.assertEquals(to, new LocalDate("2012-04-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testEffectiveEndDate() throws Exception {
+ final LocalDate firstBCD = new LocalDate(2012, 7, 16);
+ final LocalDate targetDate = new LocalDate(2012, 8, 16);
+ final BillingPeriod billingPeriod = BillingPeriod.MONTHLY;
+
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(firstBCD, null, targetDate, 16, billingPeriod);
+ final LocalDate effectiveEndDate = billingIntervalDetail.getEffectiveEndDate();
+ Assert.assertEquals(effectiveEndDate, new LocalDate(2012, 9, 16));
+ }
+
+ @Test(groups = "fast")
+ public void testLastBCD() throws Exception {
+ final LocalDate start = new LocalDate(2012, 7, 16);
+ final LocalDate endDate = new LocalDate(2012, 9, 15); // so we get effectiveEndDate on 9/15
+ final LocalDate targetDate = new LocalDate(2012, 8, 16);
+
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, endDate, targetDate, 16, BillingPeriod.MONTHLY);
+ final LocalDate lastBCD = billingIntervalDetail.getLastBillingCycleDate();
+ Assert.assertEquals(lastBCD, new LocalDate(2012, 8, 16));
+ }
+
+ @Test(groups = "fast")
+ public void testLastBCDShouldNotBeBeforePreviousBCD() throws Exception {
+ final LocalDate start = new LocalDate("2012-07-16");
+ final int bcdLocal = 15;
+
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, null, start, bcdLocal, BillingPeriod.MONTHLY);
+ final LocalDate lastBCD = billingIntervalDetail.getLastBillingCycleDate();
+ Assert.assertEquals(lastBCD, new LocalDate("2012-08-15"));
+ }
+
+ @Test(groups = "fast")
+ public void testBCD31StartingWith30DayMonth() throws Exception {
+ final LocalDate start = new LocalDate("2012-04-30");
+ final LocalDate targetDate = new LocalDate("2012-04-30");
+ final LocalDate end = null;
+ final int bcdLocal = 31;
+
+ final BillingIntervalDetail billingIntervalDetail = new BillingIntervalDetail(start, end, targetDate, bcdLocal, BillingPeriod.MONTHLY);
+ final LocalDate effectiveEndDate = billingIntervalDetail.getEffectiveEndDate();
+ Assert.assertEquals(effectiveEndDate, new LocalDate("2012-05-31"));
+ }
+
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInvoiceDateUtils.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInvoiceDateUtils.java
new file mode 100644
index 0000000..485329b
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInvoiceDateUtils.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.generator;
+
+import java.math.BigDecimal;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+
+public class TestInvoiceDateUtils extends InvoiceTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testLastBCDShouldNotBeBeforePreviousBCD() throws Exception {
+ final LocalDate from = new LocalDate("2012-07-16");
+ final LocalDate previousBCD = new LocalDate("2012-08-15");
+ final int bcdLocal = 15;
+ final LocalDate lastBCD = InvoiceDateUtils.calculateLastBillingCycleDateBefore(from, previousBCD, bcdLocal, BillingPeriod.MONTHLY);
+ Assert.assertEquals(lastBCD, new LocalDate("2012-08-15"));
+ }
+
+ @Test(groups = "fast")
+ public void testNextBCDShouldNotBeInThePast() throws Exception {
+ final LocalDate from = new LocalDate("2012-07-16");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(from, 15);
+ Assert.assertEquals(to, new LocalDate("2012-08-15"));
+ }
+
+ @Test(groups = "fast")
+ public void testProRationAfterLastBillingCycleDate() throws Exception {
+ final LocalDate endDate = new LocalDate("2012-06-02");
+ final LocalDate previousBillThroughDate = new LocalDate("2012-03-02");
+ final BigDecimal proration = InvoiceDateUtils.calculateProRationAfterLastBillingCycleDate(endDate, previousBillThroughDate, BillingPeriod.MONTHLY);
+ Assert.assertEquals(proration, new BigDecimal("2.967741935"));
+ }
+
+ @Test(groups = "fast")
+ public void testBeforeBCDWithAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-02");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-03-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testEqualBCDWithAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-03");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-04-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testAfterBCDWithAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-04");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-04-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testBeforeBCDWithOnOrAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-02");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-03-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testEqualBCDWithOnOrAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-03");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-03-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testAfterBCDWithOnOrAfter() throws Exception {
+ final LocalDate from = new LocalDate("2012-03-04");
+ final LocalDate to = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(from, 3);
+ Assert.assertEquals(to, new LocalDate("2012-04-03"));
+ }
+
+ @Test(groups = "fast")
+ public void testEffectiveEndDate() throws Exception {
+ final LocalDate firstBCD = new LocalDate(2012, 7, 16);
+ final LocalDate targetDate = new LocalDate(2012, 8, 16);
+ final BillingPeriod billingPeriod = BillingPeriod.MONTHLY;
+ final LocalDate effectiveEndDate = InvoiceDateUtils.calculateEffectiveEndDate(firstBCD, targetDate, billingPeriod);
+ // TODO should that be 2012-09-15?
+ Assert.assertEquals(effectiveEndDate, new LocalDate(2012, 9, 16));
+ }
+
+ @Test(groups = "fast")
+ public void testLastBCD() throws Exception {
+ final LocalDate firstBCD = new LocalDate(2012, 7, 16);
+ final LocalDate effectiveEndDate = new LocalDate(2012, 9, 15);
+ final BillingPeriod billingPeriod = BillingPeriod.MONTHLY;
+ final LocalDate lastBCD = InvoiceDateUtils.calculateLastBillingCycleDateBefore(effectiveEndDate, firstBCD, 16, billingPeriod);
+ Assert.assertEquals(lastBCD, new LocalDate(2012, 8, 16));
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateNbOfBillingPeriods() throws Exception {
+ final LocalDate firstBCD = new LocalDate(2012, 7, 16);
+ final LocalDate lastBCD = new LocalDate(2012, 9, 16);
+ final BillingPeriod billingPeriod = BillingPeriod.MONTHLY;
+ final int numberOfWholeBillingPeriods = InvoiceDateUtils.calculateNumberOfWholeBillingPeriods(firstBCD, lastBCD, billingPeriod);
+ Assert.assertEquals(numberOfWholeBillingPeriods, 2);
+ }
+}
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
new file mode 100644
index 0000000..e601ca8
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModule.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.glue;
+
+import org.killbill.billing.util.glue.MemoryGlobalLockerModule;
+import org.mockito.Mockito;
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.catalog.glue.CatalogModule;
+import org.killbill.billing.invoice.TestInvoiceHelper;
+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.CustomFieldModule;
+import org.killbill.billing.util.glue.NotificationQueueModule;
+import org.killbill.billing.util.glue.TagStoreModule;
+import org.killbill.billing.junction.BillingInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+
+
+public class TestInvoiceModule extends DefaultInvoiceModule {
+
+ public TestInvoiceModule(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ private void installExternalApis() {
+ bind(SubscriptionBaseInternalApi.class).toInstance(Mockito.mock(SubscriptionBaseInternalApi.class));
+ bind(BillingInternalApi.class).toInstance(Mockito.mock(BillingInternalApi.class));
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ install(new CallContextModule());
+ install(new MemoryGlobalLockerModule());
+
+ install(new CatalogModule(configSource));
+ install(new CacheModule(configSource));
+ install(new TemplateModule());
+ install(new EmailModule(configSource));
+
+ install(new NotificationQueueModule(configSource));
+ install(new TagStoreModule());
+ install(new CustomFieldModule());
+
+ installExternalApis();
+
+ bind(TestInvoiceHelper.class).asEagerSingleton();
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModuleNoDB.java b/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModuleNoDB.java
new file mode 100644
index 0000000..2dfc564
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModuleNoDB.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.glue;
+
+import java.math.BigDecimal;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.currency.api.CurrencyConversion;
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.currency.api.CurrencyConversionException;
+import org.killbill.billing.currency.api.Rate;
+import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.dao.MockInvoiceDao;
+import org.killbill.billing.mock.glue.MockNonEntityDaoModule;
+import org.killbill.billing.util.bus.InMemoryBusModule;
+import org.killbill.billing.account.api.AccountInternalApi;
+
+public class TestInvoiceModuleNoDB extends TestInvoiceModule {
+
+ public TestInvoiceModuleNoDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ protected void installInvoiceDao() {
+ bind(InvoiceDao.class).to(MockInvoiceDao.class);
+ }
+
+ @Override
+ public void configure() {
+ super.configure();
+ install(new GuicyKillbillTestNoDBModule());
+ install(new MockNonEntityDaoModule());
+ install(new InMemoryBusModule(configSource));
+
+ bind(AccountInternalApi.class).toInstance(Mockito.mock(AccountInternalApi.class));
+ bind(AccountUserApi.class).toInstance(Mockito.mock(AccountUserApi.class));
+
+ installCurrencyConversionApi();
+ }
+
+ private void installCurrencyConversionApi() {
+ final CurrencyConversionApi currencyConversionApi = Mockito.mock(CurrencyConversionApi.class);
+ final CurrencyConversion currencyConversion = Mockito.mock(CurrencyConversion.class);
+ final Set<Rate> rates = new HashSet<Rate>();
+ rates.add(new Rate() {
+ @Override
+ public Currency getBaseCurrency() {
+ return Currency.USD;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return Currency.BRL;
+ }
+
+ @Override
+ public BigDecimal getValue() {
+ return new BigDecimal("0.4234");
+ }
+
+ @Override
+ public DateTime getConversionDate() {
+ return new DateTime(DateTimeZone.UTC);
+ }
+ });
+ Mockito.when(currencyConversion.getRates()).thenReturn(rates);
+ try {
+ Mockito.when(currencyConversionApi.getCurrencyConversion(Mockito.<Currency>any(), Mockito.<DateTime>any())).thenReturn(currencyConversion);
+ } catch (CurrencyConversionException e) {
+ throw new RuntimeException(e);
+ }
+
+ bind(CurrencyConversionApi.class).toInstance(currencyConversionApi);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModuleWithEmbeddedDb.java b/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModuleWithEmbeddedDb.java
new file mode 100644
index 0000000..7304cc5
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/glue/TestInvoiceModuleWithEmbeddedDb.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.glue;
+
+import org.mockito.Mockito;
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
+import org.killbill.billing.account.glue.DefaultAccountModule;
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.invoice.InvoiceListener;
+import org.killbill.billing.invoice.TestInvoiceNotificationQListener;
+import org.killbill.billing.util.glue.BusModule;
+import org.killbill.billing.util.glue.MetricsModule;
+import org.killbill.billing.util.glue.NonEntityDaoModule;
+
+public class TestInvoiceModuleWithEmbeddedDb extends TestInvoiceModule {
+
+ public TestInvoiceModuleWithEmbeddedDb(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void installInvoiceListener() {
+ bind(InvoiceListener.class).to(TestInvoiceNotificationQListener.class).asEagerSingleton();
+ bind(TestInvoiceNotificationQListener.class).asEagerSingleton();
+ }
+
+ @Override
+ public void configure() {
+ super.configure();
+ install(new DefaultAccountModule(configSource));
+ install(new GuicyKillbillTestWithEmbeddedDBModule());
+ install(new NonEntityDaoModule());
+ install(new MetricsModule());
+ install(new BusModule(configSource));
+
+ bind(CurrencyConversionApi.class).toInstance(Mockito.mock(CurrencyConversionApi.class));
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
new file mode 100644
index 0000000..3d5a583
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice;
+
+import java.net.URL;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.invoice.api.InvoiceMigrationApi;
+import org.killbill.billing.invoice.api.InvoicePaymentApi;
+import org.killbill.billing.invoice.api.InvoiceUserApi;
+import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.generator.InvoiceGenerator;
+import org.killbill.billing.invoice.glue.TestInvoiceModuleNoDB;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.Clock;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.junction.BillingInternalApi;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public abstract class InvoiceTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ private static final Logger log = LoggerFactory.getLogger(InvoiceTestSuiteNoDB.class);
+
+ @Inject
+ protected PersistentBus bus;
+ @Inject
+ protected CacheControllerDispatcher controllerDispatcher;
+ @Inject
+ protected InvoiceUserApi invoiceUserApi;
+ @Inject
+ protected InvoicePaymentApi invoicePaymentApi;
+ @Inject
+ protected InvoiceMigrationApi migrationApi;
+ @Inject
+ protected InvoiceGenerator generator;
+ @Inject
+ protected BillingInternalApi billingApi;
+ @Inject
+ protected AccountInternalApi accountApi;
+ @Inject
+ protected SubscriptionBaseInternalApi subscriptionApi;
+ @Inject
+ protected BusService busService;
+ @Inject
+ protected TagUserApi tagUserApi;
+ @Inject
+ protected GlobalLocker locker;
+ @Inject
+ protected Clock clock;
+ @Inject
+ protected InternalCallContextFactory internalCallContextFactory;
+ @Inject
+ protected InvoiceInternalApi invoiceInternalApi;
+ @Inject
+ protected InvoiceDao invoiceDao;
+ @Inject
+ protected TestInvoiceHelper invoiceUtil;
+ @Inject
+ protected CurrencyConversionApi currencyConversionApi;
+
+ private void loadSystemPropertiesFromClasspath(final String resource) {
+ final URL url = InvoiceTestSuiteNoDB.class.getResource(resource);
+ Assert.assertNotNull(url);
+
+ configSource.merge(url);
+ }
+
+ @BeforeClass(groups = "fast")
+ protected void beforeClass() throws Exception {
+ loadSystemPropertiesFromClasspath("/resource.properties");
+
+ final Injector injector = Guice.createInjector(new TestInvoiceModuleNoDB(configSource));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() {
+ bus.start();
+ }
+
+ @AfterMethod(groups = "fast")
+ public void afterMethod() {
+ bus.stop();
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteWithEmbeddedDB.java b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..429119d
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice;
+
+import java.net.URL;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.billing.invoice.api.DefaultInvoiceService;
+import org.killbill.billing.invoice.api.InvoiceMigrationApi;
+import org.killbill.billing.invoice.api.InvoicePaymentApi;
+import org.killbill.billing.invoice.api.InvoiceService;
+import org.killbill.billing.invoice.api.InvoiceUserApi;
+import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.generator.InvoiceGenerator;
+import org.killbill.billing.invoice.glue.TestInvoiceModuleWithEmbeddedDb;
+import org.killbill.billing.invoice.notification.NextBillingDateNotifier;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.Clock;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.junction.BillingInternalApi;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public abstract class InvoiceTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
+
+ private static final Logger log = LoggerFactory.getLogger(InvoiceTestSuiteWithEmbeddedDB.class);
+
+ protected static final Currency accountCurrency = Currency.USD;
+
+ @Inject
+ protected InvoiceService invoiceService;
+ @Inject
+ protected PersistentBus bus;
+ @Inject
+ protected CacheControllerDispatcher controllerDispatcher;
+ @Inject
+ protected InvoiceUserApi invoiceUserApi;
+ @Inject
+ protected InvoicePaymentApi invoicePaymentApi;
+ @Inject
+ protected InvoiceMigrationApi migrationApi;
+ @Inject
+ protected InvoiceGenerator generator;
+ @Inject
+ protected BillingInternalApi billingApi;
+ @Inject
+ protected AccountUserApi accountUserApi;
+ @Inject
+ protected AccountInternalApi accountApi;
+ @Inject
+ protected SubscriptionBaseInternalApi subscriptionApi;
+ @Inject
+ protected BusService busService;
+ @Inject
+ protected InvoiceDao invoiceDao;
+ @Inject
+ protected NonEntityDao nonEntityDao;
+ @Inject
+ protected TagUserApi tagUserApi;
+ @Inject
+ protected GlobalLocker locker;
+ @Inject
+ protected Clock clock;
+ @Inject
+ protected InternalCallContextFactory internalCallContextFactory;
+ @Inject
+ protected InvoiceInternalApi invoiceInternalApi;
+ @Inject
+ protected NextBillingDateNotifier nextBillingDateNotifier;
+ @Inject
+ protected NotificationQueueService notificationQueueService;
+ @Inject
+ protected TestInvoiceHelper invoiceUtil;
+ @Inject
+ protected TestInvoiceNotificationQListener testInvoiceNotificationQListener;
+
+ private void loadSystemPropertiesFromClasspath(final String resource) {
+ final URL url = InvoiceTestSuiteNoDB.class.getResource(resource);
+ Assert.assertNotNull(url);
+
+ configSource.merge(url);
+ }
+
+ @BeforeClass(groups = "slow")
+ protected void beforeClass() throws Exception {
+ loadSystemPropertiesFromClasspath("/resource.properties");
+
+ final Injector injector = Guice.createInjector(new TestInvoiceModuleWithEmbeddedDb(configSource));
+ injector.injectMembers(this);
+ }
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ controllerDispatcher.clearAll();
+ bus.start();
+ restartInvoiceService(invoiceService);
+ }
+
+ private void restartInvoiceService(final InvoiceService invoiceService) throws Exception {
+ ((DefaultInvoiceService) invoiceService).initialize();
+ ((DefaultInvoiceService) invoiceService).start();
+ }
+
+ private void stopInvoiceService(final InvoiceService invoiceService) throws Exception {
+ ((DefaultInvoiceService) invoiceService).stop();
+ }
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ bus.stop();
+ stopInvoiceService(invoiceService);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/MockBillingEventSet.java b/invoice/src/test/java/org/killbill/billing/invoice/MockBillingEventSet.java
new file mode 100644
index 0000000..615acc0
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/MockBillingEventSet.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+
+public class MockBillingEventSet extends TreeSet<BillingEvent> implements BillingEventSet {
+
+ private static final long serialVersionUID = 1L;
+
+ private boolean isAccountInvoiceOff;
+ private List<UUID> subscriptionIdsWithAutoInvoiceOff = new ArrayList<UUID>();
+
+ public void addSubscriptionWithAutoInvoiceOff(final UUID subscriptionId) {
+ subscriptionIdsWithAutoInvoiceOff.add(subscriptionId);
+ }
+
+ @Override
+ public boolean isAccountAutoInvoiceOff() {
+ return isAccountInvoiceOff;
+ }
+
+ @Override
+ public List<UUID> getSubscriptionIdsWithAutoInvoiceOff() {
+ return subscriptionIdsWithAutoInvoiceOff;
+ }
+
+ public void setAccountInvoiceOff(final boolean isAccountInvoiceOff) {
+ this.isAccountInvoiceOff = isAccountInvoiceOff;
+ }
+
+ public void setSubscriptionIdsWithAutoInvoiceOff(final List<UUID> subscriptionIdsWithAutoInvoiceOff) {
+ this.subscriptionIdsWithAutoInvoiceOff = subscriptionIdsWithAutoInvoiceOff;
+ }
+
+ public void clearSubscriptionsWithAutoInvoiceOff() {
+ subscriptionIdsWithAutoInvoiceOff.clear();
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/model/TestExternalChargeInvoiceItem.java b/invoice/src/test/java/org/killbill/billing/invoice/model/TestExternalChargeInvoiceItem.java
new file mode 100644
index 0000000..9bd2154
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/model/TestExternalChargeInvoiceItem.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public class TestExternalChargeInvoiceItem extends InvoiceTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final UUID id = UUID.randomUUID();
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID accountId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final String description = UUID.randomUUID().toString();
+ final LocalDate effectiveDate = clock.getUTCToday();
+ final BigDecimal amount = BigDecimal.TEN;
+ final Currency currency = Currency.GBP;
+ final ExternalChargeInvoiceItem item = new ExternalChargeInvoiceItem(id, invoiceId, accountId, bundleId, description,
+ effectiveDate, amount, currency);
+ Assert.assertEquals(item.getAccountId(), accountId);
+ Assert.assertEquals(item.getAmount(), amount);
+ Assert.assertEquals(item.getBundleId(), bundleId);
+ Assert.assertEquals(item.getCurrency(), currency);
+ Assert.assertEquals(item.getInvoiceItemType(), InvoiceItemType.EXTERNAL_CHARGE);
+ Assert.assertEquals(item.getPlanName(), description);
+ Assert.assertNull(item.getEndDate());
+ Assert.assertNull(item.getLinkedItemId());
+ Assert.assertNull(item.getPhaseName());
+ Assert.assertNull(item.getRate());
+ Assert.assertNull(item.getSubscriptionId());
+
+ Assert.assertEquals(item, item);
+
+ final ExternalChargeInvoiceItem otherItem = new ExternalChargeInvoiceItem(id, invoiceId, UUID.randomUUID(), bundleId,
+ description, effectiveDate, amount, currency);
+ Assert.assertNotEquals(otherItem, item);
+
+ // Check comparison (done by start date)
+ final ExternalChargeInvoiceItem itemBefore = new ExternalChargeInvoiceItem(id, invoiceId, accountId, bundleId, description,
+ effectiveDate.minusDays(1), amount, currency);
+ Assert.assertFalse(itemBefore.matches(item));
+ final ExternalChargeInvoiceItem itemAfter = new ExternalChargeInvoiceItem(id, invoiceId, accountId, bundleId, description,
+ effectiveDate.plusDays(1), amount, currency);
+ Assert.assertFalse(itemAfter.matches(item));
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/model/TestInAdvanceBillingMode.java b/invoice/src/test/java/org/killbill/billing/invoice/model/TestInAdvanceBillingMode.java
new file mode 100644
index 0000000..693537d
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/model/TestInAdvanceBillingMode.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+
+public class TestInAdvanceBillingMode extends InvoiceTestSuiteNoDB {
+
+ private static final DateTimeZone TIMEZONE = DateTimeZone.forID("Pacific/Pitcairn");
+ private static final BillingPeriod BILLING_PERIOD = BillingPeriod.MONTHLY;
+
+ @Test(groups = "fast")
+ public void testItemShouldNotStartInThePast() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 7, 16);
+ final LocalDate endDate = new LocalDate(2012, 7, 16);
+ final LocalDate targetDate = new LocalDate(2012, 7, 16);
+ final int billingCycleDayLocal = 15;
+
+ final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+ verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateSimpleInvoiceItemWithNoEndDate() throws Exception {
+ final LocalDate startDate = new LocalDate(new DateTime("2012-07-17T02:25:33.000Z", DateTimeZone.UTC), TIMEZONE);
+ final LocalDate endDate = null;
+ final LocalDate targetDate = new LocalDate(2012, 7, 16);
+ final int billingCycleDayLocal = 15;
+
+ final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+ expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 15));
+
+ verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateSimpleInvoiceItemWithBCDBeforeStartDay() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 7, 16);
+ final LocalDate endDate = new LocalDate(2012, 8, 16);
+ final LocalDate targetDate = new LocalDate(2012, 7, 16);
+ final int billingCycleDayLocal = 15;
+
+ final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+ expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 15));
+
+ verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateSimpleInvoiceItemWithBCDEqualsStartDay() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 7, 16);
+ final LocalDate endDate = new LocalDate(2012, 8, 16);
+ final LocalDate targetDate = new LocalDate(2012, 7, 16);
+ final int billingCycleDayLocal = 16;
+
+ final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+ expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 16));
+
+ verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateSimpleInvoiceItemWithBCDAfterStartDay() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 7, 16);
+ final LocalDate endDate = new LocalDate(2012, 8, 16);
+ final LocalDate targetDate = new LocalDate(2012, 7, 16);
+ final int billingCycleDayLocal = 17;
+
+ final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+ expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 7, 17));
+
+ verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateSimpleInvoiceItemWithBCDBeforeStartDayWithTargetDateIn3Months() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 7, 16);
+ final LocalDate endDate = null;
+ final LocalDate targetDate = new LocalDate(2012, 10, 16);
+ final int billingCycleDayLocal = 15;
+
+ final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+ expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 15));
+ expectedDates.put(new LocalDate(2012, 8, 15), new LocalDate(2012, 9, 15));
+ expectedDates.put(new LocalDate(2012, 9, 15), new LocalDate(2012, 10, 15));
+ expectedDates.put(new LocalDate(2012, 10, 15), new LocalDate(2012, 11, 15));
+
+ verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateSimpleInvoiceItemWithBCDEqualsStartDayWithTargetDateIn3Months() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 7, 16);
+ final LocalDate endDate = null;
+ final LocalDate targetDate = new LocalDate(2012, 10, 16);
+ final int billingCycleDayLocal = 16;
+
+ final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+ expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 8, 16));
+ expectedDates.put(new LocalDate(2012, 8, 16), new LocalDate(2012, 9, 16));
+ expectedDates.put(new LocalDate(2012, 9, 16), new LocalDate(2012, 10, 16));
+ expectedDates.put(new LocalDate(2012, 10, 16), new LocalDate(2012, 11, 16));
+
+ verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateSimpleInvoiceItemWithBCDAfterStartDayWithTargetDateIn3Months() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 7, 16);
+ final LocalDate endDate = null;
+ final LocalDate targetDate = new LocalDate(2012, 10, 16);
+ final int billingCycleDayLocal = 17;
+
+ final LinkedHashMap<LocalDate, LocalDate> expectedDates = new LinkedHashMap<LocalDate, LocalDate>();
+ expectedDates.put(new LocalDate(2012, 7, 16), new LocalDate(2012, 7, 17));
+ expectedDates.put(new LocalDate(2012, 7, 17), new LocalDate(2012, 8, 17));
+ expectedDates.put(new LocalDate(2012, 8, 17), new LocalDate(2012, 9, 17));
+ expectedDates.put(new LocalDate(2012, 9, 17), new LocalDate(2012, 10, 17));
+
+ verifyInvoiceItems(startDate, endDate, targetDate, TIMEZONE, billingCycleDayLocal, BILLING_PERIOD, expectedDates);
+ }
+
+ private void verifyInvoiceItems(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate,
+ final DateTimeZone dateTimeZone, final int billingCycleDayLocal, final BillingPeriod billingPeriod,
+ final LinkedHashMap<LocalDate, LocalDate> expectedDates) throws InvalidDateSequenceException {
+ final InAdvanceBillingMode billingMode = new InAdvanceBillingMode();
+
+ final List<RecurringInvoiceItemData> invoiceItems = billingMode.calculateInvoiceItemData(startDate, endDate, targetDate, billingCycleDayLocal, billingPeriod);
+
+ int i = 0;
+ for (final LocalDate periodStartDate : expectedDates.keySet()) {
+ Assert.assertEquals(invoiceItems.get(i).getStartDate(), periodStartDate);
+ Assert.assertEquals(invoiceItems.get(i).getEndDate(), expectedDates.get(periodStartDate));
+ Assert.assertTrue(invoiceItems.get(0).getNumberOfCycles().compareTo(BigDecimal.ONE) <= 0);
+ i++;
+ }
+ Assert.assertEquals(invoiceItems.size(), i);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/model/TestItemAdjInvoiceItem.java b/invoice/src/test/java/org/killbill/billing/invoice/model/TestItemAdjInvoiceItem.java
new file mode 100644
index 0000000..eccdb7f
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/model/TestItemAdjInvoiceItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+
+public class TestItemAdjInvoiceItem extends InvoiceTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testType() throws Exception {
+ final InvoiceItem invoiceItem = new ItemAdjInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(),
+ new LocalDate(2010, 1, 1), new BigDecimal("7.00"), Currency.USD,
+ UUID.randomUUID());
+ Assert.assertEquals(invoiceItem.getInvoiceItemType(), InvoiceItemType.ITEM_ADJ);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/notification/MockNextBillingDateNotifier.java b/invoice/src/test/java/org/killbill/billing/invoice/notification/MockNextBillingDateNotifier.java
new file mode 100644
index 0000000..6211d02
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/notification/MockNextBillingDateNotifier.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+public class MockNextBillingDateNotifier implements NextBillingDateNotifier {
+
+ @Override
+ public void initialize() {
+ // do nothing
+ }
+
+ @Override
+ public void start() {
+ // do nothing
+ }
+
+ @Override
+ public void stop() {
+ // do nothing
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/notification/MockNextBillingDatePoster.java b/invoice/src/test/java/org/killbill/billing/invoice/notification/MockNextBillingDatePoster.java
new file mode 100644
index 0000000..224cbc0
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/notification/MockNextBillingDatePoster.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+public class MockNextBillingDatePoster implements NextBillingDatePoster {
+
+ @Override
+ public void insertNextBillingNotificationFromTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final UUID accountId,
+ final UUID subscriptionId, final DateTime futureNotificationTime, final UUID userToken) {
+ }
+
+ @Override
+ public void insertNextBillingNotification(final UUID accountId, final UUID subscriptionId, final DateTime futureNotificationTime, final UUID userToken) {
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
new file mode 100644
index 0000000..cb6c553
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.notification;
+
+import java.util.UUID;
+import java.util.concurrent.Callable;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
+import org.killbill.billing.invoice.api.DefaultInvoiceService;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.clock.ClockMock;
+
+import static com.jayway.awaitility.Awaitility.await;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+public class TestNextBillingDateNotifier extends InvoiceTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testInvoiceNotifier() throws Exception {
+
+ final UUID accountId = UUID.randomUUID();
+ final SubscriptionBase subscription = invoiceUtil.createSubscription();
+ final UUID subscriptionId = subscription.getId();
+ final DateTime now = clock.getUTCNow();
+
+
+ final NotificationQueue nextBillingQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME, DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
+
+
+ nextBillingQueue.recordFutureNotification(now, new NextBillingDateNotificationKey(subscriptionId), internalCallContext.getUserToken(), internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+
+ // Move time in the future after the notification effectiveDate
+ ((ClockMock) clock).setDeltaFromReality(3000);
+
+ await().atMost(1, MINUTES).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return testInvoiceNotificationQListener.getEventCount() == 1;
+ }
+ });
+
+ Assert.assertEquals(testInvoiceNotificationQListener.getEventCount(), 1);
+ Assert.assertEquals(testInvoiceNotificationQListener.getLatestSubscriptionId(), subscriptionId);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceItemFormatter.java b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceItemFormatter.java
new file mode 100644
index 0000000..fdd398f
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceItemFormatter.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.template.formatters;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.joda.time.format.DateTimeFormat;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeSuite;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+import org.killbill.billing.util.LocaleUtils;
+import org.killbill.billing.util.email.templates.MustacheTemplateEngine;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+public class TestDefaultInvoiceItemFormatter extends InvoiceTestSuiteNoDB {
+
+ private TranslatorConfig config;
+ private MustacheTemplateEngine templateEngine;
+
+ @Override
+ @BeforeClass(groups = "fast")
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ config = new ConfigurationObjectFactory(System.getProperties()).build(TranslatorConfig.class);
+ templateEngine = new MustacheTemplateEngine();
+ }
+
+ @Test(groups = "fast")
+ public void testBasicUSD() throws Exception {
+ final FixedPriceInvoiceItem fixedItemUSD = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ new LocalDate(), new BigDecimal("-1114.751625346"), Currency.USD);
+ checkOutput(fixedItemUSD, "{{#invoiceItem}}<td class=\"amount\">{{formattedAmount}}</td>{{/invoiceItem}}",
+ "<td class=\"amount\">($1,114.75)</td>", LocaleUtils.toLocale("en_US"));
+ }
+
+ @Test(groups = "fast")
+ public void testFormattedAmount() throws Exception {
+ final FixedPriceInvoiceItem fixedItemEUR = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ new LocalDate(), new BigDecimal("1499.95"), Currency.EUR);
+ checkOutput(fixedItemEUR, "{{#invoiceItem}}<td class=\"amount\">{{formattedAmount}}</td>{{/invoiceItem}}",
+ "<td class=\"amount\">1 499,95 €</td>", Locale.FRANCE);
+
+ final FixedPriceInvoiceItem fixedItemUSD = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ new LocalDate(), new BigDecimal("-1114.751625346"), Currency.USD);
+ checkOutput(fixedItemUSD, "{{#invoiceItem}}<td class=\"amount\">{{formattedAmount}}</td>{{/invoiceItem}}", "<td class=\"amount\">($1,114.75)</td>");
+
+ // Check locale/currency mismatch (locale is set at the account level)
+ final FixedPriceInvoiceItem fixedItemGBP = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ new LocalDate(), new BigDecimal("8.07"), Currency.GBP);
+ checkOutput(fixedItemGBP, "{{#invoiceItem}}<td class=\"amount\">{{formattedAmount}}</td>{{/invoiceItem}}",
+ "<td class=\"amount\">8,07 GBP</td>", Locale.FRANCE);
+ }
+
+ @Test(groups = "fast")
+ public void testNullEndDate() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 12, 1);
+ final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ startDate, BigDecimal.TEN, Currency.USD);
+ checkOutput(fixedItem,
+ "{{#invoiceItem}}<td>{{formattedStartDate}}{{#formattedEndDate}} - {{formattedEndDate}}{{/formattedEndDate}}</td>{{/invoiceItem}}",
+ "<td>Dec 1, 2012</td>");
+ }
+
+ @Test(groups = "fast")
+ public void testNonNullEndDate() throws Exception {
+ final LocalDate startDate = new LocalDate(2012, 12, 1);
+ final LocalDate endDate = new LocalDate(2012, 12, 31);
+ final RecurringInvoiceItem recurringItem = new RecurringInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ startDate, endDate, BigDecimal.TEN, BigDecimal.TEN, Currency.USD);
+ checkOutput(recurringItem,
+ "{{#invoiceItem}}<td>{{formattedStartDate}}{{#formattedEndDate}} - {{formattedEndDate}}{{/formattedEndDate}}</td>{{/invoiceItem}}",
+ "<td>Dec 1, 2012 - Dec 31, 2012</td>");
+ }
+
+ private void checkOutput(final InvoiceItem invoiceItem, final String template, final String expected) {
+ checkOutput(invoiceItem, template, expected, Locale.US);
+ }
+
+ private void checkOutput(final InvoiceItem invoiceItem, final String template, final String expected, final Locale locale) {
+ final Map<String, Object> data = new HashMap<String, Object>();
+ data.put("invoiceItem", new DefaultInvoiceItemFormatter(config, invoiceItem, DateTimeFormat.mediumDate(), locale));
+
+ final String formattedText = templateEngine.executeTemplateText(template, data);
+ Assert.assertEquals(formattedText, expected);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestHtmlInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/TestHtmlInvoiceGenerator.java
new file mode 100644
index 0000000..d5e59c7
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestHtmlInvoiceGenerator.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeSuite;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
+import org.killbill.billing.invoice.template.HtmlInvoiceGenerator;
+import org.killbill.billing.invoice.template.formatters.DefaultInvoiceFormatterFactory;
+import org.killbill.billing.util.email.templates.MustacheTemplateEngine;
+import org.killbill.billing.util.email.templates.TemplateEngine;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+public class TestHtmlInvoiceGenerator extends InvoiceTestSuiteNoDB {
+
+ private HtmlInvoiceGenerator g;
+
+ @Override
+ @BeforeClass(groups = "fast")
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ final TranslatorConfig config = new ConfigurationObjectFactory(System.getProperties()).build(TranslatorConfig.class);
+ final TemplateEngine templateEngine = new MustacheTemplateEngine();
+ final InvoiceFormatterFactory factory = new DefaultInvoiceFormatterFactory();
+ g = new HtmlInvoiceGenerator(factory, templateEngine, config, null);
+ }
+
+ @Test(groups = "fast")
+ public void testGenerateInvoice() throws Exception {
+ final String output = g.generateInvoice(createAccount(), createInvoice(), false);
+ Assert.assertNotNull(output);
+ }
+
+ @Test(groups = "fast")
+ public void testGenerateEmptyInvoice() throws Exception {
+ final Invoice invoice = Mockito.mock(Invoice.class);
+ final String output = g.generateInvoice(createAccount(), invoice, false);
+ Assert.assertNotNull(output);
+ }
+
+ @Test(groups = "fast")
+ public void testGenerateNullInvoice() throws Exception {
+ final String output = g.generateInvoice(createAccount(), null, false);
+ Assert.assertNull(output);
+ }
+
+ private Account createAccount() {
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getExternalKey()).thenReturn("1234abcd");
+ Mockito.when(account.getName()).thenReturn("Jim Smith");
+ Mockito.when(account.getFirstNameLength()).thenReturn(3);
+ Mockito.when(account.getEmail()).thenReturn("jim.smith@mail.com");
+ Mockito.when(account.getLocale()).thenReturn(Locale.US.toString());
+ Mockito.when(account.getAddress1()).thenReturn("123 Some Street");
+ Mockito.when(account.getAddress2()).thenReturn("Apt 456");
+ Mockito.when(account.getCity()).thenReturn("Some City");
+ Mockito.when(account.getStateOrProvince()).thenReturn("Some State");
+ Mockito.when(account.getPostalCode()).thenReturn("12345-6789");
+ Mockito.when(account.getCountry()).thenReturn("USA");
+ Mockito.when(account.getPhone()).thenReturn("123-456-7890");
+
+ return account;
+ }
+
+ private Invoice createInvoice() {
+ final LocalDate startDate = new LocalDate(new DateTime().minusMonths(1), DateTimeZone.UTC);
+ final LocalDate endDate = new LocalDate(DateTimeZone.UTC);
+
+ final BigDecimal price1 = new BigDecimal("29.95");
+ final BigDecimal price2 = new BigDecimal("59.95");
+ final Invoice dummyInvoice = Mockito.mock(Invoice.class);
+ Mockito.when(dummyInvoice.getInvoiceDate()).thenReturn(startDate);
+ Mockito.when(dummyInvoice.getInvoiceNumber()).thenReturn(42);
+ Mockito.when(dummyInvoice.getCurrency()).thenReturn(Currency.USD);
+ Mockito.when(dummyInvoice.getChargedAmount()).thenReturn(price1.add(price2));
+ Mockito.when(dummyInvoice.getPaidAmount()).thenReturn(BigDecimal.ZERO);
+ Mockito.when(dummyInvoice.getBalance()).thenReturn(price1.add(price2));
+
+ final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
+ items.add(createInvoiceItem(price1, "Domain 1", startDate, endDate, "ning-plus"));
+ items.add(createInvoiceItem(price2, "Domain 2", startDate, endDate, "ning-pro"));
+ Mockito.when(dummyInvoice.getInvoiceItems()).thenReturn(items);
+
+ return dummyInvoice;
+ }
+
+ private InvoiceItem createInvoiceItem(final BigDecimal amount, final String networkName, final LocalDate startDate,
+ final LocalDate endDate, final String planName) {
+ final InvoiceItem item = Mockito.mock(InvoiceItem.class);
+ Mockito.when(item.getAmount()).thenReturn(amount);
+ Mockito.when(item.getStartDate()).thenReturn(startDate);
+ Mockito.when(item.getEndDate()).thenReturn(endDate);
+ Mockito.when(item.getPlanName()).thenReturn(planName);
+ Mockito.when(item.getDescription()).thenReturn(networkName);
+
+ return item;
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
new file mode 100644
index 0000000..42d69de
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.catalog.MockPlan;
+import org.killbill.billing.catalog.MockPlanPhase;
+import org.killbill.billing.catalog.api.BillingPeriod;
+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.clock.ClockMock;
+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.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.junction.BillingEventSet;
+import org.killbill.billing.junction.BillingModeType;
+import org.killbill.billing.util.timezone.DateAndTimeZoneContext;
+
+public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
+
+ private Account account;
+ private SubscriptionBase subscription;
+ private InternalCallContext context;
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ account = invoiceUtil.createAccount(callContext);
+ subscription = invoiceUtil.createSubscription();
+ context = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+ }
+
+ @Test(groups = "slow")
+ public void testDryRunInvoice() throws InvoiceApiException, AccountApiException {
+ final UUID accountId = account.getId();
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final Plan plan = MockPlan.createBicycleNoTrialEvergreen1USD();
+ final PlanPhase planPhase = MockPlanPhase.create1USDMonthlyEvergreen();
+ final DateTime effectiveDate = new DateTime().minusDays(1);
+ final Currency currency = Currency.USD;
+ final BigDecimal fixedPrice = null;
+ events.add(invoiceUtil.createMockBillingEvent(account, subscription, effectiveDate, plan, planPhase,
+ fixedPrice, BigDecimal.ONE, currency, BillingPeriod.MONTHLY, 1,
+ BillingModeType.IN_ADVANCE, "", 1L, SubscriptionBaseTransitionType.CREATE));
+
+ Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
+
+ final DateTime target = new DateTime();
+
+ final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
+ final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
+ nonEntityDao, invoiceNotifier, locker, busService.getBus(),
+ clock);
+
+ Invoice invoice = dispatcher.processAccount(accountId, target, true, context);
+ Assert.assertNotNull(invoice);
+
+ List<InvoiceModelDao> invoices = invoiceDao.getInvoicesByAccount(context);
+ Assert.assertEquals(invoices.size(), 0);
+
+ // Try it again to double check
+ invoice = dispatcher.processAccount(accountId, target, true, context);
+ Assert.assertNotNull(invoice);
+
+ invoices = invoiceDao.getInvoicesByAccount(context);
+ Assert.assertEquals(invoices.size(), 0);
+
+ // This time no dry run
+ invoice = dispatcher.processAccount(accountId, target, false, context);
+ Assert.assertNotNull(invoice);
+
+ invoices = invoiceDao.getInvoicesByAccount(context);
+ Assert.assertEquals(invoices.size(), 1);
+ }
+
+ @Test(groups = "slow")
+ public void testWithOverdueEvents() throws Exception {
+ final BillingEventSet events = new MockBillingEventSet();
+
+ // Initial trial
+ final MockPlan bicycleTrialEvergreen1USD = MockPlan.createBicycleTrialEvergreen1USD();
+ events.add(invoiceUtil.createMockBillingEvent(account, subscription, new DateTime("2012-05-01T00:03:42.000Z"), bicycleTrialEvergreen1USD,
+ new MockPlanPhase(bicycleTrialEvergreen1USD, PhaseType.TRIAL), BigDecimal.ZERO, null, account.getCurrency(), BillingPeriod.NO_BILLING_PERIOD,
+ 31, BillingModeType.IN_ADVANCE, "CREATE", 1L, SubscriptionBaseTransitionType.CREATE));
+ // Phase change to evergreen
+ events.add(invoiceUtil.createMockBillingEvent(account, subscription, new DateTime("2012-05-31T00:03:42.000Z"), bicycleTrialEvergreen1USD,
+ new MockPlanPhase(bicycleTrialEvergreen1USD, PhaseType.EVERGREEN), null, new BigDecimal("249.95"), account.getCurrency(), BillingPeriod.MONTHLY,
+ 31, BillingModeType.IN_ADVANCE, "PHASE", 2L, SubscriptionBaseTransitionType.PHASE));
+ // Overdue period
+ events.add(invoiceUtil.createMockBillingEvent(account, subscription, new DateTime("2012-07-15T00:00:00.000Z"), bicycleTrialEvergreen1USD,
+ new MockPlanPhase(bicycleTrialEvergreen1USD, PhaseType.EVERGREEN), null, null, account.getCurrency(), BillingPeriod.NO_BILLING_PERIOD,
+ 31, BillingModeType.IN_ADVANCE, "", 0L, SubscriptionBaseTransitionType.START_BILLING_DISABLED));
+ events.add(invoiceUtil.createMockBillingEvent(account, subscription, new DateTime("2012-07-25T00:00:00.000Z"), bicycleTrialEvergreen1USD,
+ new MockPlanPhase(bicycleTrialEvergreen1USD, PhaseType.EVERGREEN), null, new BigDecimal("249.95"), account.getCurrency(), BillingPeriod.MONTHLY,
+ 31, BillingModeType.IN_ADVANCE, "", 1L, SubscriptionBaseTransitionType.END_BILLING_DISABLED));
+ // Upgrade after the overdue period
+ final MockPlan jetTrialEvergreen1000USD = MockPlan.createJetTrialEvergreen1000USD();
+ events.add(invoiceUtil.createMockBillingEvent(account, subscription, new DateTime("2012-07-25T00:04:00.000Z"), jetTrialEvergreen1000USD,
+ new MockPlanPhase(jetTrialEvergreen1000USD, PhaseType.EVERGREEN), null, new BigDecimal("1000"), account.getCurrency(), BillingPeriod.MONTHLY,
+ 31, BillingModeType.IN_ADVANCE, "CHANGE", 3L, SubscriptionBaseTransitionType.CHANGE));
+
+ Mockito.when(billingApi.getBillingEventsForAccountAndUpdateAccountBCD(Mockito.<UUID>any(), Mockito.<InternalCallContext>any())).thenReturn(events);
+ final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
+ final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
+ nonEntityDao, invoiceNotifier, locker, busService.getBus(),
+ clock);
+
+ final Invoice invoice = dispatcher.processAccount(account.getId(), new DateTime("2012-07-30T00:00:00.000Z"), false, context);
+ Assert.assertNotNull(invoice);
+
+ final List<InvoiceItem> invoiceItems = invoice.getInvoiceItems();
+ Assert.assertEquals(invoiceItems.size(), 4);
+ Assert.assertEquals(invoiceItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ Assert.assertEquals(invoiceItems.get(0).getStartDate(), new LocalDate("2012-05-01"));
+ Assert.assertNull(invoiceItems.get(0).getEndDate());
+ Assert.assertEquals(invoiceItems.get(0).getAmount(), BigDecimal.ZERO);
+ Assert.assertNull(invoiceItems.get(0).getRate());
+
+ Assert.assertEquals(invoiceItems.get(1).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ Assert.assertEquals(invoiceItems.get(1).getStartDate(), new LocalDate("2012-05-31"));
+ Assert.assertEquals(invoiceItems.get(1).getEndDate(), new LocalDate("2012-06-30"));
+ Assert.assertEquals(invoiceItems.get(1).getAmount(), new BigDecimal("249.95"));
+ Assert.assertEquals(invoiceItems.get(1).getRate(), new BigDecimal("249.95"));
+
+ Assert.assertEquals(invoiceItems.get(2).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ Assert.assertEquals(invoiceItems.get(2).getStartDate(), new LocalDate("2012-06-30"));
+ Assert.assertEquals(invoiceItems.get(2).getEndDate(), new LocalDate("2012-07-15"));
+ Assert.assertEquals(invoiceItems.get(2).getAmount(), new BigDecimal("124.98"));
+ Assert.assertEquals(invoiceItems.get(2).getRate(), new BigDecimal("249.95"));
+
+ Assert.assertEquals(invoiceItems.get(3).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ Assert.assertEquals(invoiceItems.get(3).getStartDate(), new LocalDate("2012-07-25"));
+ Assert.assertEquals(invoiceItems.get(3).getEndDate(), new LocalDate("2012-07-31"));
+ Assert.assertEquals(invoiceItems.get(3).getAmount(), new BigDecimal("193.55"));
+ Assert.assertEquals(invoiceItems.get(3).getRate(), new BigDecimal("1000"));
+
+ // Verify common fields
+ for (final InvoiceItem item : invoiceItems) {
+ Assert.assertEquals(item.getAccountId(), account.getId());
+ Assert.assertEquals(item.getBundleId(), subscription.getBundleId());
+ Assert.assertEquals(item.getCurrency(), account.getCurrency());
+ Assert.assertEquals(item.getInvoiceId(), invoice.getId());
+ Assert.assertNull(item.getLinkedItemId());
+ Assert.assertEquals(item.getSubscriptionId(), subscription.getId());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testCreateNextFutureNotificationDate() throws Exception {
+
+
+ final LocalDate startDate = new LocalDate("2012-10-26");
+ final LocalDate endDate = new LocalDate("2012-11-26");
+
+
+ ((ClockMock) clock).setTime(new DateTime(2012, 10, 13, 1, 12, 23, DateTimeZone.UTC));
+
+ final DateAndTimeZoneContext dateAndTimeZoneContext = new DateAndTimeZoneContext(clock.getUTCNow(), DateTimeZone.forID("Pacific/Pitcairn"), clock);
+
+ final InvoiceItemModelDao item = new InvoiceItemModelDao(UUID.randomUUID(), clock.getUTCNow(), InvoiceItemType.RECURRING, UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(),
+ "planName", "phaseName", startDate, endDate, new BigDecimal("23.9"), new BigDecimal("23.9"), Currency.EUR, null);
+
+ final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
+ final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountApi, billingApi, subscriptionApi, invoiceDao,
+ nonEntityDao, invoiceNotifier, locker, busService.getBus(),
+ clock);
+
+ final DateTime expectedBefore = clock.getUTCNow();
+ final Map<UUID, DateTime> result = dispatcher.createNextFutureNotificationDate(Collections.singletonList(item), dateAndTimeZoneContext);
+ final DateTime expectedAfter = clock.getUTCNow();
+
+ Assert.assertEquals(result.size(), 1);
+
+ final DateTime receivedDate = result.get(item.getSubscriptionId());
+
+ final LocalDate receivedTargetDate = new LocalDate(receivedDate, DateTimeZone.forID("Pacific/Pitcairn"));
+ Assert.assertEquals(receivedTargetDate, endDate);
+
+ Assert.assertTrue(receivedDate.compareTo(new DateTime(2012, 11, 27, 1, 12, 23, DateTimeZone.UTC)) <= 0);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceNotificationQListener.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceNotificationQListener.java
new file mode 100644
index 0000000..9cda9fa
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceNotificationQListener.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice;
+
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+
+public class TestInvoiceNotificationQListener extends InvoiceListener {
+
+ int eventCount = 0;
+ UUID latestSubscriptionId = null;
+
+ @Inject
+ public TestInvoiceNotificationQListener(final AccountInternalApi accountApi, final Clock clock, final InternalCallContextFactory internalCallContextFactory, final InvoiceDispatcher dispatcher) {
+ super(accountApi, clock, internalCallContextFactory, null, dispatcher);
+ }
+
+ @Override
+ public void handleNextBillingDateEvent(final UUID subscriptionId, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ eventCount++;
+ latestSubscriptionId = subscriptionId;
+ }
+
+ public int getEventCount() {
+ return eventCount;
+ }
+
+ public UUID getLatestSubscriptionId() {
+ return latestSubscriptionId;
+ }
+
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/GenericProRationTests.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/GenericProRationTests.java
new file mode 100644
index 0000000..ee2cec5
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/GenericProRationTests.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.annual;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.tests.inAdvance.GenericProRationTestBase;
+
+public class GenericProRationTests extends GenericProRationTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.ANNUAL;
+ }
+
+ @Override
+ protected BigDecimal getDaysInTestPeriod() {
+ return THREE_HUNDRED_AND_SIXTY_FIVE;
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestDoubleProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestDoubleProRation.java
new file mode 100644
index 0000000..ffb2501
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestDoubleProRation.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.annual;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.FOURTEEN;
+import static org.killbill.billing.invoice.TestInvoiceHelper.ONE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.THREE_HUNDRED_AND_SIXTY_FIVE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.THREE_HUNDRED_AND_SIXTY_SIX;
+import static org.killbill.billing.invoice.TestInvoiceHelper.TWELVE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.TWO;
+
+public class TestDoubleProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.ANNUAL;
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 1, 27);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateInFirstProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 7);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 1, 27);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnFirstBillingCycleDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 15);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 1, 27);
+
+ final BigDecimal expectedValue = ONE.add(FOURTEEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateInFullBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 22);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 1, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnSecondBillingCycleDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 1, 15);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 1, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(THREE_HUNDRED_AND_SIXTY_SIX, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateInSecondProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 1, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 1, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(THREE_HUNDRED_AND_SIXTY_SIX, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 1, 27);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 1, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(THREE_HUNDRED_AND_SIXTY_SIX, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateAfterEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 3, 7);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 1, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(THREE_HUNDRED_AND_SIXTY_SIX, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRationWithMultiplePeriods_TargetDateInSecondFullBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 2, 26);
+ final LocalDate endDate = invoiceUtil.buildDate(2013, 4, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(TWO);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestLeadingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestLeadingProRation.java
new file mode 100644
index 0000000..ffed530
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestLeadingProRation.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.annual;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public class TestLeadingProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.ANNUAL;
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 1);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateInProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 4);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateOnFirstBillingDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateAfterFirstBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 2, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateInProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 4);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 2, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateOnFirstBillingDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 13);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 2, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateInFinalBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 10);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 2, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateOnEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 2, 13);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 2, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateAfterEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 4, 10);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 2, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestProRation.java
new file mode 100644
index 0000000..49a42fe
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestProRation.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.annual;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.Days;
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public class TestProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.ANNUAL;
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_PrecedingProRation() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 31);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 24);
+
+ // THREE_HUNDRED_AND_FOURTY_NINE is number of days between startDate and expected first billing cycle date (2012, 1, 15);
+ final BigDecimal expectedValue = THREE_HUNDRED_AND_FOURTY_NINE.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_PrecedingProRation_CrossingYearBoundary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 12, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 1, 13);
+
+ // THREE_HUNDRED_AND_FOURTY_NINE is number of days between startDate and expected first billing cycle date (2011, 12, 4);
+ final BigDecimal expectedValue = ONE.add(THREE_HUNDRED_AND_FIFTY_FOUR.divide(THREE_HUNDRED_AND_SIXTY_FIVE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 4, expectedValue);
+ }
+
+ // TODO Test fails, needs to be investigated
+ @Test(groups = "fast", enabled = false)
+ public void testSinglePlanDoubleProRation() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 10);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 3, 4);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 4, 5);
+
+ final BigDecimal expectedValue = BigDecimal.ZERO;
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestTrailingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestTrailingProRation.java
new file mode 100644
index 0000000..fe245ec
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/annual/TestTrailingProRation.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.annual;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public class TestTrailingProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.ANNUAL;
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 6, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 6, 17);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateInFirstBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 6, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 6, 20);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateAtEndOfFirstBillingCycle() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 6, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 6, 17);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(THREE_HUNDRED_AND_SIXTY_SIX, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateInProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 6, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 6, 18);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(THREE_HUNDRED_AND_SIXTY_SIX, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateOnEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 6, 25);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(THREE_HUNDRED_AND_SIXTY_SIX, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, endDate, 17, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateAfterEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 6, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 7, 30);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(THREE_HUNDRED_AND_SIXTY_SIX, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/GenericProRationTestBase.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/GenericProRationTestBase.java
new file mode 100644
index 0000000..8c3774d
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/GenericProRationTestBase.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public abstract class GenericProRationTestBase extends ProRationInAdvanceTestBase {
+
+ /**
+ * used for testing cancellation in less than a single billing period
+ *
+ * @return BigDecimal the number of days in the billing period beginning 2011/1/1
+ */
+ protected abstract BigDecimal getDaysInTestPeriod();
+
+ @Test(groups = "fast")
+ public void testSinglePlan_OnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 15);
+
+ testCalculateNumberOfBillingCycles(startDate, startDate, 15, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_LessThanOnePeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 1);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_OnePeriodLessADayAfterStart() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 15);
+ final LocalDate targetDate = startDate.plusMonths(getBillingPeriod().getNumberOfMonths()).plusDays(-1);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_ExactlyOnePeriodAfterStart() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 15);
+ final LocalDate targetDate = startDate.plusMonths(getBillingPeriod().getNumberOfMonths());
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, TWO);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_SlightlyMoreThanOnePeriodAfterStart() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 15);
+ final LocalDate targetDate = startDate.plusMonths(getBillingPeriod().getNumberOfMonths()).plusDays(1);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, TWO);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_CrossingYearBoundary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 12, 15);
+ final LocalDate oneCycleLater = startDate.plusMonths(getBillingPeriod().getNumberOfMonths());
+
+ // test just before the billing cycle day
+ testCalculateNumberOfBillingCycles(startDate, oneCycleLater.plusDays(-1), 15, ONE);
+
+ // test on the billing cycle day
+ testCalculateNumberOfBillingCycles(startDate, oneCycleLater, 15, TWO);
+
+ // test just after the billing cycle day
+ testCalculateNumberOfBillingCycles(startDate, oneCycleLater.plusDays(1), 15, TWO);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_StartingMidFebruary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 15);
+ final LocalDate targetDate = startDate.plusMonths(getBillingPeriod().getNumberOfMonths());
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, TWO);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_StartingMidFebruaryOfLeapYear() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2012, 2, 15);
+ final LocalDate targetDate = startDate.plusMonths(getBillingPeriod().getNumberOfMonths());
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, TWO);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_MovingForwardThroughTime() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 31);
+ BigDecimal expectedValue = ONE;
+
+ for (int i = 1; i <= 12; i++) {
+ final LocalDate oneCycleLater = startDate.plusMonths(i * getBillingPeriod().getNumberOfMonths());
+ // test just before the billing cycle day
+ testCalculateNumberOfBillingCycles(startDate, oneCycleLater.plusDays(-1), 31, expectedValue);
+
+ expectedValue = expectedValue.add(ONE);
+
+ // test on the billing cycle day
+ testCalculateNumberOfBillingCycles(startDate, oneCycleLater, 31, expectedValue);
+
+ // test just after the billing cycle day
+ testCalculateNumberOfBillingCycles(startDate, oneCycleLater.plusDays(1), 31, expectedValue);
+ }
+ }
+
+ // tests for cancellation in less than one period, beginning Jan 1
+ @Test(groups = "fast")
+ public void testCancelledBeforeOnePeriod_TargetDateInStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 1, 15);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(getDaysInTestPeriod(), KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 1, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testCancelledBeforeOnePeriod_TargetDateInSubscriptionPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 7);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 1, 15);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(getDaysInTestPeriod(), KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 1, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testCancelledBeforeOnePeriod_TargetDateOnEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 15);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 1, 15);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(getDaysInTestPeriod(), KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 1, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testCancelledBeforeOnePeriod_TargetDateAfterEndDateButInFirstBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 1, 15);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(getDaysInTestPeriod(), KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 1, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testCancelledBeforeOnePeriod_TargetDateAtEndOfFirstBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 1, 15);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(getDaysInTestPeriod(), KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 1, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testCancelledBeforeOnePeriod_TargetDateAfterFirstBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 5);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 1, 15);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(getDaysInTestPeriod(), KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 1, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/GenericProRationTests.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/GenericProRationTests.java
new file mode 100644
index 0000000..7358125
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/GenericProRationTests.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.monthly;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.tests.inAdvance.GenericProRationTestBase;
+
+public class GenericProRationTests extends GenericProRationTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.MONTHLY;
+ }
+
+ @Override
+ protected BigDecimal getDaysInTestPeriod() {
+ return THIRTY_ONE;
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestDoubleProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestDoubleProRation.java
new file mode 100644
index 0000000..64583ae
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestDoubleProRation.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.monthly;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public class TestDoubleProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.MONTHLY;
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 2, 27);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateInFirstProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 7);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 2, 27);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnFirstBillingCycleDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 15);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 2, 27);
+
+ final BigDecimal expectedValue = ONE.add(FOURTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateInFullBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 22);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 2, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnSecondBillingCycleDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 27);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 2, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(TWENTY_EIGHT, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateInSecondProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 26);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 2, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(TWENTY_EIGHT, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 27);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 2, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(TWENTY_EIGHT, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateAfterEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 7);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 2, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(TWENTY_EIGHT, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRationWithMultiplePeriods_TargetDateInSecondFullBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 26);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(TWO);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestLeadingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestLeadingProRation.java
new file mode 100644
index 0000000..53ab598
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestLeadingProRation.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.monthly;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public class TestLeadingProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.MONTHLY;
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 1);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateInProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 4);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateOnFirstBillingDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateAfterFirstBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD).add(THREE);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateInProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 4);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateOnFirstBillingDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 13);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateInFinalBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 10);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD).add(TWO);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateOnEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 13);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD).add(TWO);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateAfterEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 10);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD).add(TWO);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestProRation.java
new file mode 100644
index 0000000..a4bd726
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestProRation.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.monthly;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.EIGHT;
+import static org.killbill.billing.invoice.TestInvoiceHelper.FIVE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.FOURTEEN;
+import static org.killbill.billing.invoice.TestInvoiceHelper.ONE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.ONE_AND_A_HALF;
+import static org.killbill.billing.invoice.TestInvoiceHelper.ONE_HALF;
+import static org.killbill.billing.invoice.TestInvoiceHelper.SEVEN;
+import static org.killbill.billing.invoice.TestInvoiceHelper.THIRTEEN;
+import static org.killbill.billing.invoice.TestInvoiceHelper.THIRTY_ONE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.THREE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.TWENTY_EIGHT;
+import static org.killbill.billing.invoice.TestInvoiceHelper.TWENTY_NINE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.TWO;
+
+public class TestProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.MONTHLY;
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_WithPhaseChange() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 10);
+ final LocalDate phaseChangeDate = invoiceUtil.buildDate(2011, 2, 24);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 6);
+
+ testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 10, ONE_HALF);
+ testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 10, ONE_HALF);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_WithPhaseChange_BeforeBillCycleDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 3);
+ final LocalDate phaseChangeDate = invoiceUtil.buildDate(2011, 2, 17);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 1);
+
+ testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 3, ONE_HALF);
+ testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 3, ONE_HALF);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_WithPhaseChange_OnBillCycleDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 3);
+ final LocalDate phaseChangeDate = invoiceUtil.buildDate(2011, 2, 17);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 3);
+
+ testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 3, ONE_HALF);
+ testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 3, ONE_AND_A_HALF);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_WithPhaseChange_AfterBillCycleDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 3);
+ final LocalDate phaseChangeDate = invoiceUtil.buildDate(2011, 2, 17);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 4);
+
+ testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 3, ONE_HALF);
+ testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 3, ONE_AND_A_HALF);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_WithChangeOfBillCycleDayToLaterDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate planChangeDate = invoiceUtil.buildDate(2011, 2, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 1);
+
+ testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 1, ONE_HALF);
+ testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 15, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_WithChangeOfBillCycleDayToEarlierDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 20);
+ final LocalDate planChangeDate = invoiceUtil.buildDate(2011, 3, 6);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 9);
+
+ testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 20, ONE_HALF);
+ testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 6, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_CrossingYearBoundary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 12, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 16);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, TWO);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_LeapYear_StartingMidFebruary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2012, 2, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 3, 15);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, TWO);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_LeapYear_StartingBeforeFebruary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2012, 1, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 2, 3);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_LeapYear_IncludingAllOfFebruary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2012, 1, 30);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 3, 1);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 30, TWO);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_ChangeBCDTo31() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate planChangeDate = invoiceUtil.buildDate(2011, 2, 14);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 1);
+
+ BigDecimal expectedValue;
+
+ expectedValue = THIRTEEN.divide(TWENTY_EIGHT, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 1, expectedValue);
+
+ expectedValue = ONE.add(FOURTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 31, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_ChangeBCD() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate planChangeDate = invoiceUtil.buildDate(2011, 2, 14);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 1);
+
+ BigDecimal expectedValue;
+
+ expectedValue = THIRTEEN.divide(TWENTY_EIGHT, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 1, expectedValue);
+
+ expectedValue = ONE.add(THIRTEEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 27, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_LeapYearFebruaryProRation() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2012, 2, 1);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 2, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 2, 19);
+
+ final BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(TWENTY_NINE, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 1, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_BeforeBillingDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 7);
+ final LocalDate changeDate = invoiceUtil.buildDate(2011, 2, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 21);
+
+ final BigDecimal expectedValue;
+
+ expectedValue = EIGHT.divide(TWENTY_EIGHT, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, changeDate, targetDate, 7, expectedValue);
+
+ testCalculateNumberOfBillingCycles(changeDate, targetDate, 15, THREE);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_OnBillingDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 7);
+ final LocalDate changeDate = invoiceUtil.buildDate(2011, 3, 7);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 21);
+
+ testCalculateNumberOfBillingCycles(startDate, changeDate, targetDate, 7, ONE);
+
+ final BigDecimal expectedValue;
+ expectedValue = EIGHT.divide(TWENTY_EIGHT, KillBillMoney.ROUNDING_METHOD).add(TWO);
+ testCalculateNumberOfBillingCycles(changeDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_AfterBillingDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 7);
+ final LocalDate changeDate = invoiceUtil.buildDate(2011, 3, 10);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 21);
+
+ BigDecimal expectedValue;
+
+ expectedValue = BigDecimal.ONE.add(THREE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, changeDate, targetDate, 7, expectedValue);
+
+ expectedValue = FIVE.divide(TWENTY_EIGHT, KillBillMoney.ROUNDING_METHOD).add(TWO);
+ testCalculateNumberOfBillingCycles(changeDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_DoubleProRation() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 31);
+ final LocalDate planChangeDate = invoiceUtil.buildDate(2011, 3, 10);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 21);
+
+ BigDecimal expectedValue;
+ expectedValue = SEVEN.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(THREE.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 7, expectedValue);
+
+ expectedValue = FIVE.divide(TWENTY_EIGHT, KillBillMoney.ROUNDING_METHOD).add(TWO);
+ testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testStartTargetEnd() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 12, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 15);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 3, 17);
+
+ final BigDecimal expectedValue = THREE.add(TWO.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestTrailingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestTrailingProRation.java
new file mode 100644
index 0000000..36ccd42
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/monthly/TestTrailingProRation.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.monthly;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.EIGHT;
+import static org.killbill.billing.invoice.TestInvoiceHelper.ONE;
+import static org.killbill.billing.invoice.TestInvoiceHelper.THIRTY_ONE;
+
+public class TestTrailingProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.MONTHLY;
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 7, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 6, 17);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateInFirstBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 7, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 6, 20);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateAtEndOfFirstBillingCycle() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 7, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 7, 17);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateInProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 7, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 7, 18);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateOnEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 7, 25);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, endDate, 17, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateAfterEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 7, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 7, 30);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(THIRTY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/ProRationInAdvanceTestBase.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/ProRationInAdvanceTestBase.java
new file mode 100644
index 0000000..f3913fc
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/ProRationInAdvanceTestBase.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance;
+
+import org.killbill.billing.invoice.model.BillingMode;
+import org.killbill.billing.invoice.model.InAdvanceBillingMode;
+import org.killbill.billing.invoice.tests.ProRationTestBase;
+
+public abstract class ProRationInAdvanceTestBase extends ProRationTestBase {
+
+ @Override
+ protected BillingMode getBillingMode() {
+ return new InAdvanceBillingMode();
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/GenericProRationTests.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/GenericProRationTests.java
new file mode 100644
index 0000000..3136399
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/GenericProRationTests.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.quarterly;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.tests.inAdvance.GenericProRationTestBase;
+
+public class GenericProRationTests extends GenericProRationTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.QUARTERLY;
+ }
+
+ @Override
+ protected BigDecimal getDaysInTestPeriod() {
+ return NINETY;
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestDoubleProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestDoubleProRation.java
new file mode 100644
index 0000000..a6dd313
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestDoubleProRation.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.quarterly;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public class TestDoubleProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.QUARTERLY;
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 27);
+
+ BigDecimal expectedValue = FOURTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateInFirstProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 7);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 27);
+
+ BigDecimal expectedValue = FOURTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnFirstBillingCycleDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 15);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 27);
+
+ BigDecimal expectedValue = ONE.add(FOURTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateInFullBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 22);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnSecondBillingCycleDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 15);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(NINETY_ONE, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateInSecondProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 26);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(NINETY_ONE, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateOnEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 4, 27);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(NINETY_ONE, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRation_TargetDateAfterEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 5, 7);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 4, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(ONE);
+ expectedValue = expectedValue.add(TWELVE.divide(NINETY_ONE, KillBillMoney.ROUNDING_METHOD));
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testDoubleProRationWithMultiplePeriods_TargetDateInSecondFullBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 6, 26);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 8, 27);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+ expectedValue = expectedValue.add(TWO);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestLeadingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestLeadingProRation.java
new file mode 100644
index 0000000..140144f
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestLeadingProRation.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.quarterly;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public class TestLeadingProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.QUARTERLY;
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 1);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateInProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 4);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateOnFirstBillingDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD).add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_Evergreen_TargetDateAfterFirstBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 6, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD).add(TWO);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 8, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateInProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 4);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 8, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateOnFirstBillingDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 13);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 8, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD).add(ONE);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateInFinalBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 8, 10);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 8, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD).add(TWO);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateOnEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 8, 13);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 8, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD).add(TWO);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testLeadingProRation_WithEndDate_TargetDateAfterEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 9, 10);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 8, 13);
+
+ final BigDecimal expectedValue;
+ expectedValue = TWELVE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD).add(TWO);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 13, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestProRation.java
new file mode 100644
index 0000000..531573c
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestProRation.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.quarterly;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.Days;
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public class TestProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.QUARTERLY;
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_WithPhaseChange() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 10);
+ final LocalDate phaseChangeDate = invoiceUtil.buildDate(2011, 2, 24);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 6);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 10, expectedValue);
+
+ // 75 is number of days between phaseChangeDate and next billing cycle date (2011, 5, 10)
+ // 89 is total number of days between the next and previous billing period (2011, 2, 10) -> (2011, 5, 10)
+ expectedValue = SEVENTY_FIVE.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 10, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_WithPhaseChange_OnBillCycleDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 3);
+ final LocalDate phaseChangeDate = invoiceUtil.buildDate(2011, 2, 17);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 5, 3);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 3, expectedValue);
+
+ expectedValue = ONE.add(SEVENTY_FIVE.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 3, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_WithPhaseChange_AfterBillCycleDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 3);
+ final LocalDate phaseChangeDate = invoiceUtil.buildDate(2011, 2, 17);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 5, 4);
+
+ BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, phaseChangeDate, targetDate, 3, expectedValue);
+
+ expectedValue = SEVENTY_FIVE.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+ testCalculateNumberOfBillingCycles(phaseChangeDate, targetDate, 3, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_WithChangeOfBillCycleDayToLaterDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate planChangeDate = invoiceUtil.buildDate(2011, 2, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 1);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 1, expectedValue);
+ testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 15, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_WithChangeOfBillCycleDayToEarlierDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 20);
+ final LocalDate planChangeDate = invoiceUtil.buildDate(2011, 3, 6);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 9);
+
+ final BigDecimal expectedValue = FOURTEEN.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 20, expectedValue);
+ testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 6, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_CrossingYearBoundary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 12, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 16);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_LeapYear_StartingMidFebruary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2012, 2, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 3, 15);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_LeapYear_StartingBeforeFebruary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2012, 1, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 2, 3);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 15, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_LeapYear_IncludingAllOfFebruary() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2012, 1, 30);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 3, 1);
+
+ testCalculateNumberOfBillingCycles(startDate, targetDate, 30, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_ChangeBCDTo31() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate planChangeDate = invoiceUtil.buildDate(2011, 2, 14);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 3, 1);
+
+ BigDecimal expectedValue;
+
+ expectedValue = THIRTEEN.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 1, expectedValue);
+
+ expectedValue = ONE.add(FOURTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 31, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_ChangeBCD() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 1);
+ final LocalDate planChangeDate = invoiceUtil.buildDate(2011, 2, 14);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 5, 1);
+
+ BigDecimal expectedValue;
+
+ expectedValue = THIRTEEN.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 1, expectedValue);
+
+ expectedValue = ONE.add(THIRTEEN.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 27, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testSinglePlan_LeapYearFebruaryProRation() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2012, 2, 1);
+ final LocalDate endDate = invoiceUtil.buildDate(2012, 2, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2012, 2, 19);
+
+ final BigDecimal expectedValue;
+ expectedValue = FOURTEEN.divide(NINETY, KillBillMoney.ROUNDING_METHOD);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 1, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_BeforeBillingDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 7);
+ final LocalDate changeDate = invoiceUtil.buildDate(2011, 2, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 9, 21);
+
+ final BigDecimal expectedValue;
+
+ expectedValue = EIGHT.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD);
+ testCalculateNumberOfBillingCycles(startDate, changeDate, targetDate, 7, expectedValue);
+
+ testCalculateNumberOfBillingCycles(changeDate, targetDate, 15, THREE);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_OnBillingDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 7);
+ final LocalDate changeDate = invoiceUtil.buildDate(2011, 5, 7);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 7, 21);
+
+ testCalculateNumberOfBillingCycles(startDate, changeDate, targetDate, 7, ONE);
+
+ final BigDecimal expectedValue;
+ expectedValue = EIGHT.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+ testCalculateNumberOfBillingCycles(changeDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_AfterBillingDay() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 2, 7);
+ final LocalDate changeDate = invoiceUtil.buildDate(2011, 5, 10);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 9, 21);
+
+ BigDecimal expectedValue;
+
+ expectedValue = ONE.add(THREE.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, changeDate, targetDate, 7, expectedValue);
+
+ expectedValue = FIVE.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD).add(TWO);
+ testCalculateNumberOfBillingCycles(changeDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testPlanChange_DoubleProRation() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 31);
+ final LocalDate planChangeDate = invoiceUtil.buildDate(2011, 5, 10);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 5, 21);
+
+ BigDecimal expectedValue;
+ // startDate, 2011, 4, 7 -> 66 days out of 2011, 1, 7, 2011, 4, 7 -> 90
+ expectedValue = SIXTY_SIX.divide(NINETY, KillBillMoney.ROUNDING_METHOD);
+ // 2011, 1, 7, planChangeDate-> 33 days out of 2011, 4, 7, 2011, 7, 7 -> 89
+ expectedValue = expectedValue.add(THIRTY_THREE.divide(NINETY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, planChangeDate, targetDate, 7, expectedValue);
+
+ expectedValue = FIVE.divide(EIGHTY_NINE, KillBillMoney.ROUNDING_METHOD).add(ONE);
+ testCalculateNumberOfBillingCycles(planChangeDate, targetDate, 15, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testStartTargetEnd() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 12, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 6, 15);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 6, 17);
+
+ final BigDecimal expectedValue = TWO.add(TWO.divide(NINETY_TWO, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 15, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestTrailingProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestTrailingProRation.java
new file mode 100644
index 0000000..32a47eb
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/quarterly/TestTrailingProRation.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance.quarterly;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.inAdvance.ProRationInAdvanceTestBase;
+import org.killbill.billing.util.currency.KillBillMoney;
+
+public class TestTrailingProRation extends ProRationInAdvanceTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.QUARTERLY;
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateOnStartDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 9, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 6, 17);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateInFirstBillingPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 9, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 6, 20);
+
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, ONE);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateAtEndOfFirstBillingCycle() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 9, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 9, 17);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(NINETY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateInProRationPeriod() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 9, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 9, 18);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(NINETY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateOnEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 9, 25);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(NINETY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, endDate, 17, expectedValue);
+ }
+
+ @Test(groups = "fast")
+ public void testTargetDateAfterEndDate() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2010, 6, 17);
+ final LocalDate endDate = invoiceUtil.buildDate(2010, 9, 25);
+ final LocalDate targetDate = invoiceUtil.buildDate(2010, 9, 30);
+
+ final BigDecimal expectedValue = ONE.add(EIGHT.divide(NINETY_ONE, KillBillMoney.ROUNDING_METHOD));
+ testCalculateNumberOfBillingCycles(startDate, endDate, targetDate, 17, expectedValue);
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/TestValidationProRation.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/TestValidationProRation.java
new file mode 100644
index 0000000..c647604
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/inAdvance/TestValidationProRation.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests.inAdvance;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.model.BillingMode;
+import org.killbill.billing.invoice.model.InAdvanceBillingMode;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.tests.ProRationTestBase;
+
+import static org.testng.Assert.assertEquals;
+
+public class TestValidationProRation extends ProRationTestBase {
+
+ @Override
+ protected BillingPeriod getBillingPeriod() {
+ return BillingPeriod.MONTHLY;
+ }
+
+ @Override
+ protected BillingMode getBillingMode() {
+ return new InAdvanceBillingMode();
+ }
+
+ @Test(groups = "fast", expectedExceptions = InvalidDateSequenceException.class)
+ public void testTargetStartEnd() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 30);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 3, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 1, 15);
+
+ calculateNumberOfBillingCycles(startDate, endDate, targetDate, 15);
+ }
+
+ @Test(groups = "fast", expectedExceptions = InvalidDateSequenceException.class)
+ public void testTargetEndStart() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 4, 30);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 3, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 15);
+
+ calculateNumberOfBillingCycles(startDate, endDate, targetDate, 15);
+ }
+
+ @Test(groups = "fast", expectedExceptions = InvalidDateSequenceException.class)
+ public void testEndTargetStart() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 3, 30);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 1, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 15);
+
+ calculateNumberOfBillingCycles(startDate, endDate, targetDate, 15);
+ }
+
+ @Test(groups = "fast", expectedExceptions = InvalidDateSequenceException.class)
+ public void testEndStartTarget() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 1, 30);
+ final LocalDate endDate = invoiceUtil.buildDate(2011, 1, 15);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 15);
+
+ calculateNumberOfBillingCycles(startDate, endDate, targetDate, 15);
+ }
+
+ @Test(groups = "fast", expectedExceptions = InvalidDateSequenceException.class)
+ public void testTargetStart() throws InvalidDateSequenceException {
+ final LocalDate startDate = invoiceUtil.buildDate(2011, 4, 30);
+ final LocalDate targetDate = invoiceUtil.buildDate(2011, 2, 15);
+
+ calculateNumberOfBillingCycles(startDate, targetDate, 15);
+ }
+}
+
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/InternationalPriceMock.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/InternationalPriceMock.java
new file mode 100644
index 0000000..803c38d
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/InternationalPriceMock.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests;
+
+import java.math.BigDecimal;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.InternationalPrice;
+import org.killbill.billing.catalog.api.Price;
+
+import static org.testng.Assert.fail;
+
+public class InternationalPriceMock implements InternationalPrice {
+
+ private final BigDecimal rate;
+
+ public InternationalPriceMock(final BigDecimal rate) {
+ this.rate = rate;
+ }
+
+ @Override
+ public Price[] getPrices() {
+ fail();
+
+ return null;
+ }
+
+ @Override
+ public BigDecimal getPrice(final Currency currency) {
+ return rate;
+ }
+
+ @Override
+ public boolean isZero() {
+ return rate.compareTo(BigDecimal.ZERO) == 0;
+ }
+
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/InvoiceTestUtils.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/InvoiceTestUtils.java
new file mode 100644
index 0000000..b21e3e9
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/InvoiceTestUtils.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.mockito.Mockito;
+import org.testng.Assert;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentType;
+import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
+import org.killbill.billing.invoice.dao.InvoiceModelDao;
+import org.killbill.billing.invoice.dao.InvoicePaymentModelDao;
+import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entity.EntityPersistenceException;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class InvoiceTestUtils {
+
+ private InvoiceTestUtils() {}
+
+ public static Invoice createAndPersistInvoice(final InvoiceDao invoiceDao,
+ final Clock clock,
+ final BigDecimal amount,
+ final Currency currency,
+ final InternalCallContext internalCallContext) {
+ try {
+ return createAndPersistInvoice(invoiceDao, clock, ImmutableList.<BigDecimal>of(amount),
+ currency, internalCallContext);
+ } catch (EntityPersistenceException e) {
+ Assert.fail(e.getMessage());
+ return null;
+ }
+ }
+
+ public static Invoice createAndPersistInvoice(final InvoiceDao invoiceDao,
+ final Clock clock,
+ final List<BigDecimal> amounts,
+ final Currency currency,
+ final InternalCallContext internalCallContext) throws EntityPersistenceException {
+ final Invoice invoice = Mockito.mock(Invoice.class);
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID accountId = UUID.randomUUID();
+
+ Mockito.when(invoice.getId()).thenReturn(invoiceId);
+ Mockito.when(invoice.getAccountId()).thenReturn(accountId);
+ Mockito.when(invoice.getInvoiceDate()).thenReturn(clock.getUTCToday());
+ Mockito.when(invoice.getTargetDate()).thenReturn(clock.getUTCToday());
+ Mockito.when(invoice.getCurrency()).thenReturn(currency);
+ Mockito.when(invoice.isMigrationInvoice()).thenReturn(false);
+
+ final List<InvoiceItem> invoiceItems = new ArrayList<InvoiceItem>();
+ final List<InvoiceItemModelDao> invoiceModelItems = new ArrayList<InvoiceItemModelDao>();
+ for (final BigDecimal amount : amounts) {
+ final InvoiceItem invoiceItem = createInvoiceItem(clock, invoiceId, accountId, amount, currency);
+ invoiceModelItems.add(new InvoiceItemModelDao(invoiceItem));
+ invoiceItems.add(invoiceItem);
+ }
+ Mockito.when(invoice.getInvoiceItems()).thenReturn(invoiceItems);
+
+ invoiceDao.createInvoice(new InvoiceModelDao(invoice), invoiceModelItems, ImmutableList.<InvoicePaymentModelDao>of(), true, ImmutableMap.<UUID, DateTime>of(), internalCallContext);
+
+ return invoice;
+ }
+
+ public static InvoiceItem createInvoiceItem(final Clock clock, final UUID invoiceId, final UUID accountId, final BigDecimal amount, final Currency currency) {
+ return new FixedPriceInvoiceItem(invoiceId, accountId, UUID.randomUUID(), UUID.randomUUID(),
+ "charge back test", "charge back phase", clock.getUTCToday(), amount, currency);
+ }
+
+ public static InvoicePayment createAndPersistPayment(final InvoiceInternalApi invoicePaymentApi,
+ final Clock clock,
+ final UUID invoiceId,
+ final BigDecimal amount,
+ final Currency currency,
+ final InternalCallContext callContext) throws InvoiceApiException {
+ final InvoicePayment payment = Mockito.mock(InvoicePayment.class);
+ Mockito.when(payment.getId()).thenReturn(UUID.randomUUID());
+ Mockito.when(payment.getType()).thenReturn(InvoicePaymentType.ATTEMPT);
+ Mockito.when(payment.getInvoiceId()).thenReturn(invoiceId);
+ Mockito.when(payment.getPaymentId()).thenReturn(UUID.randomUUID());
+ Mockito.when(payment.getPaymentCookieId()).thenReturn(UUID.randomUUID());
+ Mockito.when(payment.getPaymentDate()).thenReturn(clock.getUTCNow());
+ Mockito.when(payment.getAmount()).thenReturn(amount);
+ Mockito.when(payment.getCurrency()).thenReturn(currency);
+ Mockito.when(payment.getProcessedCurrency()).thenReturn(currency);
+
+ invoicePaymentApi.notifyOfPayment(payment, callContext);
+
+ return payment;
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/ProRationTestBase.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/ProRationTestBase.java
new file mode 100644
index 0000000..2785e4f
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/ProRationTestBase.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests;
+
+import static org.killbill.billing.invoice.TestInvoiceHelper.*;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+import org.killbill.billing.invoice.model.BillingMode;
+import org.killbill.billing.invoice.model.InvalidDateSequenceException;
+import org.killbill.billing.invoice.model.RecurringInvoiceItemData;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+public abstract class ProRationTestBase extends InvoiceTestSuiteNoDB {
+
+ protected abstract BillingMode getBillingMode();
+
+ protected abstract BillingPeriod getBillingPeriod();
+
+ protected void testCalculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate targetDate, final int billingCycleDay, final BigDecimal expectedValue) throws InvalidDateSequenceException {
+ try {
+ final BigDecimal numberOfBillingCycles;
+ numberOfBillingCycles = calculateNumberOfBillingCycles(startDate, targetDate, billingCycleDay);
+
+ assertEquals(numberOfBillingCycles.compareTo(expectedValue), 0, "Actual: " + numberOfBillingCycles.toString() + "; expected: " + expectedValue.toString());
+ } catch (InvalidDateSequenceException idse) {
+ throw idse;
+ } catch (Exception e) {
+ fail("Unexpected exception: " + e.getMessage());
+ }
+ }
+
+ protected void testCalculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate, final int billingCycleDay, final BigDecimal expectedValue) throws InvalidDateSequenceException {
+ try {
+ final BigDecimal numberOfBillingCycles;
+ numberOfBillingCycles = calculateNumberOfBillingCycles(startDate, endDate, targetDate, billingCycleDay);
+
+ assertEquals(numberOfBillingCycles.compareTo(expectedValue), 0, "Actual: " + numberOfBillingCycles.toString() + "; expected: " + expectedValue.toString());
+ } catch (InvalidDateSequenceException idse) {
+ throw idse;
+ } catch (Exception e) {
+ fail("Unexpected exception: " + e.getMessage());
+ }
+ }
+
+ protected BigDecimal calculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate endDate, final LocalDate targetDate, final int billingCycleDay) throws InvalidDateSequenceException {
+ final List<RecurringInvoiceItemData> items = getBillingMode().calculateInvoiceItemData(startDate, endDate, targetDate, billingCycleDay, getBillingPeriod());
+
+ BigDecimal numberOfBillingCycles = ZERO;
+ for (final RecurringInvoiceItemData item : items) {
+ numberOfBillingCycles = numberOfBillingCycles.add(item.getNumberOfCycles());
+ }
+
+ return numberOfBillingCycles;
+ }
+
+ protected BigDecimal calculateNumberOfBillingCycles(final LocalDate startDate, final LocalDate targetDate, final int billingCycleDay) throws InvalidDateSequenceException {
+ final List<RecurringInvoiceItemData> items = getBillingMode().calculateInvoiceItemData(startDate, null, targetDate, billingCycleDay, getBillingPeriod());
+
+ BigDecimal numberOfBillingCycles = ZERO;
+ for (final RecurringInvoiceItemData item : items) {
+ numberOfBillingCycles = numberOfBillingCycles.add(item.getNumberOfCycles());
+ }
+
+ return numberOfBillingCycles;
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tests/TestChargeBacks.java b/invoice/src/test/java/org/killbill/billing/invoice/tests/TestChargeBacks.java
new file mode 100644
index 0000000..8d5069e
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tests/TestChargeBacks.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tests;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoicePayment;
+
+import static org.killbill.billing.invoice.tests.InvoiceTestUtils.createAndPersistInvoice;
+import static org.killbill.billing.invoice.tests.InvoiceTestUtils.createAndPersistPayment;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
+
+ private static final BigDecimal FIFTEEN = new BigDecimal("15.00");
+ private static final BigDecimal THIRTY = new BigDecimal("30.00");
+ private static final BigDecimal ONE_MILLION = new BigDecimal("1000000.00");
+
+ private static final Currency CURRENCY = Currency.EUR;
+
+ @Test(groups = "slow")
+ public void testCompleteChargeBack() throws InvoiceApiException {
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock, THIRTY, CURRENCY, internalCallContext);
+ final InvoicePayment payment = createAndPersistPayment(invoiceInternalApi, clock, invoice.getId(), THIRTY, CURRENCY, internalCallContext);
+
+ // create a full charge back
+ invoicePaymentApi.createChargeback(payment.getId(), THIRTY, callContext);
+
+ // check amount owed
+ final BigDecimal amount = invoicePaymentApi.getRemainingAmountPaid(payment.getId(), callContext);
+ assertTrue(amount.compareTo(BigDecimal.ZERO) == 0);
+ }
+
+ @Test(groups = "slow")
+ public void testPartialChargeBack() throws InvoiceApiException {
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock, THIRTY, CURRENCY, internalCallContext);
+ final InvoicePayment payment = createAndPersistPayment(invoiceInternalApi, clock, invoice.getId(), THIRTY, CURRENCY, internalCallContext);
+
+ // create a partial charge back
+ invoicePaymentApi.createChargeback(payment.getId(), FIFTEEN, callContext);
+
+ // check amount owed
+ final BigDecimal amount = invoicePaymentApi.getRemainingAmountPaid(payment.getId(), callContext);
+ assertTrue(amount.compareTo(FIFTEEN) == 0);
+ }
+
+ @Test(groups = "slow", expectedExceptions = InvoiceApiException.class)
+ public void testChargeBackLargerThanPaymentAmount() throws InvoiceApiException {
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock, THIRTY, CURRENCY, internalCallContext);
+ final InvoicePayment payment = createAndPersistPayment(invoiceInternalApi, clock, invoice.getId(), THIRTY, CURRENCY, internalCallContext);
+
+ // create a large charge back
+ invoicePaymentApi.createChargeback(payment.getId(), ONE_MILLION, callContext);
+ fail("Expected a failure...");
+ }
+
+ @Test(groups = "slow", expectedExceptions = InvoiceApiException.class)
+ public void testNegativeChargeBackAmount() throws InvoiceApiException {
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock, THIRTY, CURRENCY, internalCallContext);
+ final InvoicePayment payment = createAndPersistPayment(invoiceInternalApi, clock, invoice.getId(), THIRTY, CURRENCY, internalCallContext);
+
+ // create a partial charge back
+ invoicePaymentApi.createChargeback(payment.getId(), BigDecimal.ONE.negate(), callContext);
+ }
+
+ @Test(groups = "slow")
+ public void testGetAccountIdFromPaymentIdHappyPath() throws InvoiceApiException {
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock, THIRTY, CURRENCY, internalCallContext);
+ final InvoicePayment payment = createAndPersistPayment(invoiceInternalApi, clock, invoice.getId(), THIRTY, CURRENCY, internalCallContext);
+ final UUID accountId = invoicePaymentApi.getAccountIdFromInvoicePaymentId(payment.getId(), callContext);
+ assertEquals(accountId, invoice.getAccountId());
+ }
+
+ @Test(groups = "slow")
+ public void testGetAccountIdFromPaymentIdBadPaymentId() throws InvoiceApiException {
+ try {
+ invoicePaymentApi.getAccountIdFromInvoicePaymentId(UUID.randomUUID(), callContext);
+ fail();
+ } catch (InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.CHARGE_BACK_COULD_NOT_FIND_ACCOUNT_ID.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testGetChargeBacksByAccountIdWithEmptyReturnSet() throws InvoiceApiException {
+ final List<InvoicePayment> chargebacks = invoicePaymentApi.getChargebacksByAccountId(UUID.randomUUID(), callContext);
+ assertNotNull(chargebacks);
+ assertEquals(chargebacks.size(), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testGetChargeBacksByAccountIdHappyPath() throws InvoiceApiException {
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock, THIRTY, CURRENCY, internalCallContext);
+ final InvoicePayment payment = createAndPersistPayment(invoiceInternalApi, clock, invoice.getId(), THIRTY, CURRENCY, internalCallContext);
+
+ // create a partial charge back
+ invoicePaymentApi.createChargeback(payment.getId(), FIFTEEN, callContext);
+
+ final List<InvoicePayment> chargebacks = invoicePaymentApi.getChargebacksByAccountId(invoice.getAccountId(), callContext);
+ assertNotNull(chargebacks);
+ assertEquals(chargebacks.size(), 1);
+ assertEquals(chargebacks.get(0).getLinkedInvoicePaymentId(), payment.getId());
+ }
+
+ @Test(groups = "slow")
+ public void testGetChargeBacksByPaymentIdWithEmptyReturnSet() throws InvoiceApiException {
+ final List<InvoicePayment> chargebacks = invoicePaymentApi.getChargebacksByPaymentId(UUID.randomUUID(), callContext);
+ assertNotNull(chargebacks);
+ assertEquals(chargebacks.size(), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testGetChargeBacksByInvoicePaymentIdHappyPath() throws InvoiceApiException {
+ final Invoice invoice = createAndPersistInvoice(invoiceDao, clock, THIRTY, CURRENCY, internalCallContext);
+ final InvoicePayment payment = createAndPersistPayment(invoiceInternalApi, clock, invoice.getId(), THIRTY, CURRENCY, internalCallContext);
+
+ // create a partial charge back
+ invoicePaymentApi.createChargeback(payment.getId(), FIFTEEN, callContext);
+
+ final List<InvoicePayment> chargebacks = invoicePaymentApi.getChargebacksByPaymentId(payment.getPaymentId(), callContext);
+ assertNotNull(chargebacks);
+ assertEquals(chargebacks.size(), 1);
+ assertEquals(chargebacks.get(0).getLinkedInvoicePaymentId(), payment.getId());
+ }
+}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestNodeInterval.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestNodeInterval.java
new file mode 100644
index 0000000..f75aad3
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestNodeInterval.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.tree;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.invoice.tree.NodeInterval.AddNodeCallback;
+import org.killbill.billing.invoice.tree.NodeInterval.BuildNodeCallback;
+import org.killbill.billing.invoice.tree.NodeInterval.SearchCallback;
+import org.killbill.billing.invoice.tree.NodeInterval.WalkCallback;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestNodeInterval /* extends InvoiceTestSuiteNoDB */ {
+
+ private AddNodeCallback CALLBACK = new DummyAddNodeCallback();
+
+ public class DummyNodeInterval extends NodeInterval {
+
+ private final UUID id;
+
+ public DummyNodeInterval() {
+ this.id = UUID.randomUUID();
+ }
+
+ public DummyNodeInterval(final NodeInterval parent, final LocalDate startDate, final LocalDate endDate) {
+ super(parent, startDate, endDate);
+ this.id = UUID.randomUUID();
+ }
+
+ public UUID getId() {
+ return id;
+ }
+ }
+
+ public class DummyAddNodeCallback implements AddNodeCallback {
+
+ @Override
+ public boolean onExistingNode(final NodeInterval existingNode) {
+ return false;
+ }
+
+ @Override
+ public boolean shouldInsertNode(final NodeInterval insertionNode) {
+ return true;
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testAddExistingItemSimple() {
+ final DummyNodeInterval root = new DummyNodeInterval();
+
+ final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
+ root.addNode(top, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+ final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+ final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+ root.addNode(firstChildLevel1, CALLBACK);
+ root.addNode(secondChildLevel1, CALLBACK);
+ root.addNode(thirdChildLevel1, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+ final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
+ final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+ root.addNode(firstChildLevel2, CALLBACK);
+ root.addNode(secondChildLevel2, CALLBACK);
+ root.addNode(thirdChildLevel2, CALLBACK);
+
+ checkNode(top, 3, root, firstChildLevel1, null);
+ checkNode(firstChildLevel1, 2, top, firstChildLevel2, secondChildLevel1);
+ checkNode(secondChildLevel1, 0, top, null, thirdChildLevel1);
+ checkNode(thirdChildLevel1, 1, top, thirdChildLevel2, null);
+
+ checkNode(firstChildLevel2, 0, firstChildLevel1, null, secondChildLevel2);
+ checkNode(secondChildLevel2, 0, firstChildLevel1, null, null);
+ checkNode(thirdChildLevel2, 0, thirdChildLevel1, null, null);
+ }
+
+ @Test(groups = "fast")
+ public void testAddExistingItemWithRebalance() {
+ final DummyNodeInterval root = new DummyNodeInterval();
+
+ final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
+ root.addNode(top, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+ final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
+ final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+ root.addNode(firstChildLevel2, CALLBACK);
+ root.addNode(secondChildLevel2, CALLBACK);
+ root.addNode(thirdChildLevel2, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+ final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+ final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+ root.addNode(firstChildLevel1, CALLBACK);
+ root.addNode(secondChildLevel1, CALLBACK);
+ root.addNode(thirdChildLevel1, CALLBACK);
+
+ checkNode(top, 3, root, firstChildLevel1, null);
+ checkNode(firstChildLevel1, 2, top, firstChildLevel2, secondChildLevel1);
+ checkNode(secondChildLevel1, 0, top, null, thirdChildLevel1);
+ checkNode(thirdChildLevel1, 1, top, thirdChildLevel2, null);
+
+ checkNode(firstChildLevel2, 0, firstChildLevel1, null, secondChildLevel2);
+ checkNode(secondChildLevel2, 0, firstChildLevel1, null, null);
+ checkNode(thirdChildLevel2, 0, thirdChildLevel1, null, null);
+ }
+
+ @Test(groups = "fast")
+ public void testBuild() {
+ final DummyNodeInterval root = new DummyNodeInterval();
+
+ final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
+ root.addNode(top, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+ final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+ final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+ root.addNode(firstChildLevel1, CALLBACK);
+ root.addNode(secondChildLevel1, CALLBACK);
+ root.addNode(thirdChildLevel1, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+ final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
+ final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+ root.addNode(firstChildLevel2, CALLBACK);
+ root.addNode(secondChildLevel2, CALLBACK);
+ root.addNode(thirdChildLevel2, CALLBACK);
+
+ final List<NodeInterval> output = new LinkedList<NodeInterval>();
+
+ // Just build the missing pieces.
+ root.build(new BuildNodeCallback() {
+ @Override
+ public void onMissingInterval(final NodeInterval curNode, final LocalDate startDate, final LocalDate endDate) {
+ output.add(createNodeInterval(startDate, endDate));
+ }
+
+ @Override
+ public void onLastNode(final NodeInterval curNode) {
+ // Nothing
+ }
+ });
+
+ final List<NodeInterval> expected = new LinkedList<NodeInterval>();
+ expected.add(createNodeInterval("2014-01-05", "2014-01-07"));
+ expected.add(createNodeInterval("2014-01-07", "2014-01-08"));
+ expected.add(createNodeInterval("2014-01-15", "2014-01-16"));
+ expected.add(createNodeInterval("2014-01-17", "2014-02-01"));
+
+ assertEquals(output.size(), expected.size());
+ checkInterval(output.get(0), expected.get(0));
+ checkInterval(output.get(1), expected.get(1));
+ }
+
+ @Test(groups = "fast")
+ public void testSearch() {
+ final DummyNodeInterval root = new DummyNodeInterval();
+
+ final DummyNodeInterval top = createNodeInterval("2014-01-01", "2014-02-01");
+ root.addNode(top, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+ final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+ final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+ root.addNode(firstChildLevel1, CALLBACK);
+ root.addNode(secondChildLevel1, CALLBACK);
+ root.addNode(thirdChildLevel1, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+ final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-5");
+ final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+ root.addNode(firstChildLevel2, CALLBACK);
+ root.addNode(secondChildLevel2, CALLBACK);
+ root.addNode(thirdChildLevel2, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel3 = createNodeInterval("2014-01-01", "2014-01-02");
+ final DummyNodeInterval secondChildLevel3 = createNodeInterval("2014-01-03", "2014-01-04");
+ root.addNode(firstChildLevel3, CALLBACK);
+ root.addNode(secondChildLevel3, CALLBACK);
+
+ final NodeInterval search1 = root.findNode(new LocalDate("2014-01-04"), new SearchCallback() {
+ @Override
+ public boolean isMatch(final NodeInterval curNode) {
+ return ((DummyNodeInterval) curNode).getId().equals(secondChildLevel3.getId());
+ }
+ });
+ checkInterval(search1, secondChildLevel3);
+
+ final NodeInterval search2 = root.findNode(new SearchCallback() {
+ @Override
+ public boolean isMatch(final NodeInterval curNode) {
+ return ((DummyNodeInterval) curNode).getId().equals(thirdChildLevel2.getId());
+ }
+ });
+ checkInterval(search2, thirdChildLevel2);
+
+ final NodeInterval nullSearch = root.findNode(new SearchCallback() {
+ @Override
+ public boolean isMatch(final NodeInterval curNode) {
+ return ((DummyNodeInterval) curNode).getId().equals("foo");
+ }
+ });
+ assertNull(nullSearch);
+ }
+
+ @Test(groups = "fast")
+ public void testWalkTree() {
+ final DummyNodeInterval root = new DummyNodeInterval();
+
+ final DummyNodeInterval firstChildLevel0 = createNodeInterval("2014-01-01", "2014-02-01");
+ root.addNode(firstChildLevel0, CALLBACK);
+
+ final DummyNodeInterval secondChildLevel0 = createNodeInterval("2014-02-01", "2014-03-01");
+ root.addNode(secondChildLevel0, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel1 = createNodeInterval("2014-01-01", "2014-01-07");
+ final DummyNodeInterval secondChildLevel1 = createNodeInterval("2014-01-08", "2014-01-15");
+ final DummyNodeInterval thirdChildLevel1 = createNodeInterval("2014-01-16", "2014-02-01");
+ root.addNode(firstChildLevel1, CALLBACK);
+ root.addNode(secondChildLevel1, CALLBACK);
+ root.addNode(thirdChildLevel1, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel2 = createNodeInterval("2014-01-01", "2014-01-03");
+ final DummyNodeInterval secondChildLevel2 = createNodeInterval("2014-01-03", "2014-01-05");
+ final DummyNodeInterval thirdChildLevel2 = createNodeInterval("2014-01-16", "2014-01-17");
+ root.addNode(firstChildLevel2, CALLBACK);
+ root.addNode(secondChildLevel2, CALLBACK);
+ root.addNode(thirdChildLevel2, CALLBACK);
+
+ final DummyNodeInterval firstChildLevel3 = createNodeInterval("2014-01-01", "2014-01-02");
+ final DummyNodeInterval secondChildLevel3 = createNodeInterval("2014-01-03", "2014-01-04");
+ root.addNode(firstChildLevel3, CALLBACK);
+ root.addNode(secondChildLevel3, CALLBACK);
+
+ final List<NodeInterval> expected = new LinkedList<NodeInterval>();
+ expected.add(root);
+ expected.add(firstChildLevel0);
+ expected.add(firstChildLevel1);
+ expected.add(firstChildLevel2);
+ expected.add(firstChildLevel3);
+ expected.add(secondChildLevel2);
+ expected.add(secondChildLevel3);
+ expected.add(secondChildLevel1);
+ expected.add(thirdChildLevel1);
+ expected.add(thirdChildLevel2);
+ expected.add(secondChildLevel0);
+
+ final List<NodeInterval> result = new LinkedList<NodeInterval>();
+ root.walkTree(new WalkCallback() {
+ @Override
+ public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
+ result.add(curNode);
+ }
+ });
+
+ assertEquals(result.size(), expected.size());
+ for (int i = 0; i < result.size(); i++) {
+ if (i == 0) {
+ assertTrue(result.get(0).isRoot());
+ checkInterval(result.get(0), createNodeInterval("2014-01-01", "2014-03-01"));
+ } else {
+ checkInterval(result.get(i), expected.get(i));
+ }
+ }
+ }
+
+ private void checkInterval(final NodeInterval real, final NodeInterval expected) {
+ assertEquals(real.getStart(), expected.getStart());
+ assertEquals(real.getEnd(), expected.getEnd());
+ }
+
+ private void checkNode(final NodeInterval node, final int expectedChildren, final DummyNodeInterval expectedParent, final DummyNodeInterval expectedLeftChild, final DummyNodeInterval expectedRightSibling) {
+ assertEquals(node.getNbChildren(), expectedChildren);
+ assertEquals(node.getParent(), expectedParent);
+ assertEquals(node.getRightSibling(), expectedRightSibling);
+ assertEquals(node.getLeftChild(), expectedLeftChild);
+ assertEquals(node.getLeftChild(), expectedLeftChild);
+ }
+
+ private DummyNodeInterval createNodeInterval(final LocalDate startDate, final LocalDate endDate) {
+ return new DummyNodeInterval(null, startDate, endDate);
+ }
+
+ private DummyNodeInterval createNodeInterval(final String startDate, final String endDate) {
+ return createNodeInterval(new LocalDate(startDate), new LocalDate(endDate));
+ }
+
+}
diff --git a/invoice/src/test/resources/org/killbill/billing/util/template/translation/InvoiceTranslation_en_US.properties b/invoice/src/test/resources/org/killbill/billing/util/template/translation/InvoiceTranslation_en_US.properties
new file mode 100644
index 0000000..02d074a
--- /dev/null
+++ b/invoice/src/test/resources/org/killbill/billing/util/template/translation/InvoiceTranslation_en_US.properties
@@ -0,0 +1,22 @@
+invoiceTitle=INVOICE
+invoiceDate=Date:
+invoiceNumber=Invoice #
+invoiceAmount=New Charges
+invoiceAmountPaid=Payment
+invoiceBalance=Balance
+
+accountOwnerName=Killbill fighter
+
+companyName=Killbill, Inc.
+companyAddress=P.O. Box 1234
+companyCityProvincePostalCode=Springfield
+companyCountry=USA
+companyUrl=http://killbilling.org
+
+invoiceItemBundleName=Weapon
+invoiceItemDescription=Description
+invoiceItemServicePeriod=Service Period
+invoiceItemAmount=Amount
+
+processedPaymentCurrency=(*) The payment was processed in
+processedPaymentRate=The rate applied was
diff --git a/invoice/src/test/resources/resource.properties b/invoice/src/test/resources/resource.properties
index b271d93..fffad07 100644
--- a/invoice/src/test/resources/resource.properties
+++ b/invoice/src/test/resources/resource.properties
@@ -1 +1 @@
-com.ning.billing.invoice.maxNumberOfMonthsInFuture = 36
+org.killbill.billing.invoice.maxNumberOfMonthsInFuture = 36
jaxrs/pom.xml 40(+20 -20)
diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index 6e7b061..bb7b693 100644
--- a/jaxrs/pom.xml
+++ b/jaxrs/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-jaxrs</artifactId>
@@ -42,25 +42,37 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>javax.ws.rs</groupId>
+ <artifactId>jsr311-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<!--
@@ -69,22 +81,10 @@
-->
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>javax.servlet</groupId>
- <artifactId>javax.servlet-api</artifactId>
- </dependency>
- <dependency>
- <groupId>javax.ws.rs</groupId>
- <artifactId>jsr311-api</artifactId>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountEmailJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountEmailJson.java
new file mode 100644
index 0000000..a0447c5
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountEmailJson.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.account.api.AccountEmail;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class AccountEmailJson extends JsonBase {
+
+ private final String accountId;
+ private final String email;
+
+ @JsonCreator
+ public AccountEmailJson(@JsonProperty("accountId") final String accountId, @JsonProperty("email") final String email) {
+ this.accountId = accountId;
+ this.email = email;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public AccountEmail toAccountEmail(final UUID accountEmailId) {
+
+ return new AccountEmail() {
+ @Override
+ public UUID getAccountId() {
+ return UUID.fromString(accountId);
+ }
+
+ @Override
+ public String getEmail() {
+ return email;
+ }
+
+ @Override
+ public UUID getId() {
+ return accountEmailId;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return null;
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("AccountEmailJson");
+ sb.append("{accountId='").append(accountId).append('\'');
+ sb.append(", email='").append(email).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final AccountEmailJson that = (AccountEmailJson) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (email != null ? !email.equals(that.email) : that.email != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = accountId != null ? accountId.hashCode() : 0;
+ result = 31 * result + (email != null ? email.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountJson.java
new file mode 100644
index 0000000..7e6ceef
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountJson.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTimeZone;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Objects;
+
+public class AccountJson extends JsonBase {
+
+ private final String accountId;
+ private final String externalKey;
+ private final BigDecimal accountCBA;
+ private final BigDecimal accountBalance;
+ private final String name;
+ private final Integer firstNameLength;
+ private final String email;
+ private final Integer billCycleDayLocal;
+ private final String currency;
+ private final String paymentMethodId;
+ private final String timeZone;
+ private final String address1;
+ private final String address2;
+ private final String postalCode;
+ private final String company;
+ private final String city;
+ private final String state;
+ private final String country;
+ private final String locale;
+ private final String phone;
+ private final Boolean isMigrated;
+ private final Boolean isNotifiedForInvoices;
+
+ public AccountJson(final Account account, final BigDecimal accountBalance, final BigDecimal accountCBA, @Nullable final AccountAuditLogs accountAuditLogs) {
+ super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForAccount()));
+ this.accountCBA = accountCBA;
+ this.accountBalance = accountBalance;
+ this.accountId = account.getId().toString();
+ this.externalKey = account.getExternalKey();
+ this.name = account.getName();
+ this.firstNameLength = account.getFirstNameLength();
+ this.email = account.getEmail();
+ this.billCycleDayLocal = account.getBillCycleDayLocal();
+ this.currency = account.getCurrency() != null ? account.getCurrency().toString() : null;
+ this.paymentMethodId = account.getPaymentMethodId() != null ? account.getPaymentMethodId().toString() : null;
+ this.timeZone = account.getTimeZone().toString();
+ this.address1 = account.getAddress1();
+ this.address2 = account.getAddress2();
+ this.postalCode = account.getPostalCode();
+ this.company = account.getCompanyName();
+ this.city = account.getCity();
+ this.state = account.getStateOrProvince();
+ this.country = account.getCountry();
+ this.locale = account.getLocale();
+ this.phone = account.getPhone();
+ this.isMigrated = account.isMigrated();
+ this.isNotifiedForInvoices = account.isNotifiedForInvoices();
+ }
+
+ @JsonCreator
+ public AccountJson(@JsonProperty("accountId") final String accountId,
+ @JsonProperty("name") final String name,
+ @JsonProperty("firstNameLength") final Integer firstNameLength,
+ @JsonProperty("externalKey") final String externalKey,
+ @JsonProperty("email") final String email,
+ @JsonProperty("billCycleDayLocal") final Integer billCycleDayLocal,
+ @JsonProperty("currency") final String currency,
+ @JsonProperty("paymentMethodId") final String paymentMethodId,
+ @JsonProperty("timeZone") final String timeZone,
+ @JsonProperty("address1") final String address1,
+ @JsonProperty("address2") final String address2,
+ @JsonProperty("postalCode") final String postalCode,
+ @JsonProperty("company") final String company,
+ @JsonProperty("city") final String city,
+ @JsonProperty("state") final String state,
+ @JsonProperty("country") final String country,
+ @JsonProperty("locale") final String locale,
+ @JsonProperty("phone") final String phone,
+ @JsonProperty("isMigrated") final Boolean isMigrated,
+ @JsonProperty("isNotifiedForInvoices") final Boolean isNotifiedForInvoices,
+ @JsonProperty("accountBalance") final BigDecimal accountBalance,
+ @JsonProperty("accountCBA") final BigDecimal accountCBA,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.accountBalance = accountBalance;
+ this.externalKey = externalKey;
+ this.accountId = accountId;
+ this.name = name;
+ this.firstNameLength = firstNameLength;
+ this.email = email;
+ this.billCycleDayLocal = billCycleDayLocal;
+ this.currency = currency;
+ this.paymentMethodId = paymentMethodId;
+ this.timeZone = timeZone;
+ this.address1 = address1;
+ this.address2 = address2;
+ this.postalCode = postalCode;
+ this.company = company;
+ this.city = city;
+ this.state = state;
+ this.country = country;
+ this.locale = locale;
+ this.phone = phone;
+ this.isMigrated = isMigrated;
+ this.isNotifiedForInvoices = isNotifiedForInvoices;
+ this.accountCBA = accountCBA;
+ }
+
+ public AccountData toAccountData() {
+ return new AccountData() {
+ @Override
+ public DateTimeZone getTimeZone() {
+ return (timeZone != null) ? DateTimeZone.forID(timeZone) : null;
+ }
+
+ @Override
+ public String getStateOrProvince() {
+ return state;
+ }
+
+ @Override
+ public String getPostalCode() {
+ return postalCode;
+ }
+
+ @Override
+ public String getPhone() {
+ return phone;
+ }
+
+ @Override
+ public Boolean isMigrated() {
+ return Objects.firstNonNull(isMigrated, false);
+ }
+
+ @Override
+ public Boolean isNotifiedForInvoices() {
+ return Objects.firstNonNull(isNotifiedForInvoices, false);
+ }
+
+ @Override
+ public UUID getPaymentMethodId() {
+ return paymentMethodId != null ? UUID.fromString(paymentMethodId) : null;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getLocale() {
+ return locale;
+ }
+
+ @Override
+ public Integer getFirstNameLength() {
+ if (firstNameLength == null && name == null) {
+ return 0;
+ } else if (firstNameLength == null) {
+ return name.length();
+ } else {
+ return firstNameLength;
+ }
+ }
+
+ @Override
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ @Override
+ public String getEmail() {
+ return email;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ if (currency == null) {
+ return null;
+ } else {
+ return Currency.valueOf(currency);
+ }
+ }
+
+ @Override
+ public String getCountry() {
+ return country;
+ }
+
+ @Override
+ public String getCompanyName() {
+ return company;
+ }
+
+ @Override
+ public String getCity() {
+ return city;
+ }
+
+ @Override
+ public Integer getBillCycleDayLocal() {
+ return billCycleDayLocal;
+ }
+
+ @Override
+ public String getAddress2() {
+ return address2;
+ }
+
+ @Override
+ public String getAddress1() {
+ return address1;
+ }
+ };
+ }
+
+ public BigDecimal getAccountBalance() {
+ return accountBalance;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ public BigDecimal getAccountCBA() {
+ return accountCBA;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Integer getFirstNameLength() {
+ return firstNameLength;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public Integer getBillCycleDayLocal() {
+ return billCycleDayLocal;
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public String getPaymentMethodId() {
+ return paymentMethodId;
+ }
+
+ public String getTimeZone() {
+ return timeZone;
+ }
+
+ public String getAddress1() {
+ return address1;
+ }
+
+ public String getAddress2() {
+ return address2;
+ }
+
+ public String getPostalCode() {
+ return postalCode;
+ }
+
+ public String getCompany() {
+ return company;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public String getLocale() {
+ return locale;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ @JsonProperty("isMigrated")
+ public Boolean isMigrated() {
+ return isMigrated;
+ }
+
+ @JsonProperty("isNotifiedForInvoices")
+ public Boolean isNotifiedForInvoices() {
+ return isNotifiedForInvoices;
+ }
+
+ @Override
+ public String toString() {
+ return "AccountJson{" +
+ "accountId='" + accountId + '\'' +
+ ", externalKey='" + externalKey + '\'' +
+ ", accountCBA=" + accountCBA +
+ ", accountBalance=" + accountBalance +
+ ", name='" + name + '\'' +
+ ", firstNameLength=" + firstNameLength +
+ ", email='" + email + '\'' +
+ ", billCycleDayLocal=" + billCycleDayLocal +
+ ", currency='" + currency + '\'' +
+ ", paymentMethodId='" + paymentMethodId + '\'' +
+ ", timeZone='" + timeZone + '\'' +
+ ", address1='" + address1 + '\'' +
+ ", address2='" + address2 + '\'' +
+ ", postalCode='" + postalCode + '\'' +
+ ", company='" + company + '\'' +
+ ", city='" + city + '\'' +
+ ", state='" + state + '\'' +
+ ", country='" + country + '\'' +
+ ", locale='" + locale + '\'' +
+ ", phone='" + phone + '\'' +
+ ", isMigrated=" + isMigrated +
+ ", isNotifiedForInvoices=" + isNotifiedForInvoices +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final AccountJson that = (AccountJson) o;
+
+ if (accountBalance != null ? accountBalance.compareTo(that.accountBalance) != 0 : that.accountBalance != null) {
+ return false;
+ }
+ if (accountCBA != null ? accountCBA.compareTo(that.accountCBA) != 0 : that.accountCBA != null) {
+ return false;
+ }
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (address1 != null ? !address1.equals(that.address1) : that.address1 != null) {
+ return false;
+ }
+ if (address2 != null ? !address2.equals(that.address2) : that.address2 != null) {
+ return false;
+ }
+ if (billCycleDayLocal != null ? !billCycleDayLocal.equals(that.billCycleDayLocal) : that.billCycleDayLocal != null) {
+ return false;
+ }
+ if (city != null ? !city.equals(that.city) : that.city != null) {
+ return false;
+ }
+ if (company != null ? !company.equals(that.company) : that.company != null) {
+ return false;
+ }
+ if (country != null ? !country.equals(that.country) : that.country != null) {
+ return false;
+ }
+ if (currency != null ? !currency.equals(that.currency) : that.currency != null) {
+ return false;
+ }
+ if (email != null ? !email.equals(that.email) : that.email != null) {
+ return false;
+ }
+ if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
+ return false;
+ }
+ if (firstNameLength != null ? !firstNameLength.equals(that.firstNameLength) : that.firstNameLength != null) {
+ return false;
+ }
+ if (isMigrated != null ? !isMigrated.equals(that.isMigrated) : that.isMigrated != null) {
+ return false;
+ }
+ if (isNotifiedForInvoices != null ? !isNotifiedForInvoices.equals(that.isNotifiedForInvoices) : that.isNotifiedForInvoices != null) {
+ return false;
+ }
+ if (locale != null ? !locale.equals(that.locale) : that.locale != null) {
+ return false;
+ }
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+ if (paymentMethodId != null ? !paymentMethodId.equals(that.paymentMethodId) : that.paymentMethodId != null) {
+ return false;
+ }
+ if (phone != null ? !phone.equals(that.phone) : that.phone != null) {
+ return false;
+ }
+ if (postalCode != null ? !postalCode.equals(that.postalCode) : that.postalCode != null) {
+ return false;
+ }
+ if (state != null ? !state.equals(that.state) : that.state != null) {
+ return false;
+ }
+ if (timeZone != null ? !timeZone.equals(that.timeZone) : that.timeZone != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = accountId != null ? accountId.hashCode() : 0;
+ result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+ result = 31 * result + (accountCBA != null ? accountCBA.hashCode() : 0);
+ result = 31 * result + (accountBalance != null ? accountBalance.hashCode() : 0);
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (firstNameLength != null ? firstNameLength.hashCode() : 0);
+ result = 31 * result + (email != null ? email.hashCode() : 0);
+ result = 31 * result + (billCycleDayLocal != null ? billCycleDayLocal.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (paymentMethodId != null ? paymentMethodId.hashCode() : 0);
+ result = 31 * result + (timeZone != null ? timeZone.hashCode() : 0);
+ result = 31 * result + (address1 != null ? address1.hashCode() : 0);
+ result = 31 * result + (address2 != null ? address2.hashCode() : 0);
+ result = 31 * result + (postalCode != null ? postalCode.hashCode() : 0);
+ result = 31 * result + (company != null ? company.hashCode() : 0);
+ result = 31 * result + (city != null ? city.hashCode() : 0);
+ result = 31 * result + (state != null ? state.hashCode() : 0);
+ result = 31 * result + (country != null ? country.hashCode() : 0);
+ result = 31 * result + (locale != null ? locale.hashCode() : 0);
+ result = 31 * result + (phone != null ? phone.hashCode() : 0);
+ result = 31 * result + (isMigrated != null ? isMigrated.hashCode() : 0);
+ result = 31 * result + (isNotifiedForInvoices != null ? isNotifiedForInvoices.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java
new file mode 100644
index 0000000..228e085
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AccountTimelineJson.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.entitlement.api.SubscriptionBundle;
+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.InvoicePayment;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.Refund;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+import org.killbill.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.Multimap;
+
+public class AccountTimelineJson {
+
+ private final AccountJson account;
+ private final List<BundleJson> bundles;
+ private final List<InvoiceJson> invoices;
+ private final List<PaymentJson> payments;
+
+ @JsonCreator
+ public AccountTimelineJson(@JsonProperty("account") final AccountJson account,
+ @JsonProperty("bundles") final List<BundleJson> bundles,
+ @JsonProperty("invoices") final List<InvoiceJson> invoices,
+ @JsonProperty("payments") final List<PaymentJson> payments) {
+ this.account = account;
+ this.bundles = bundles;
+ this.invoices = invoices;
+ this.payments = payments;
+ }
+
+ private String getBundleExternalKey(final UUID invoiceId, final List<Invoice> invoices, final List<SubscriptionBundle> bundles) {
+ for (final Invoice cur : invoices) {
+ if (cur.getId().equals(invoiceId)) {
+ return getBundleExternalKey(cur, bundles);
+ }
+ }
+ return null;
+ }
+
+ private String getBundleExternalKey(final Invoice invoice, final List<SubscriptionBundle> bundles) {
+ final Set<UUID> b = new HashSet<UUID>();
+ for (final InvoiceItem cur : invoice.getInvoiceItems()) {
+ b.add(cur.getBundleId());
+ }
+ boolean first = true;
+ final StringBuilder tmp = new StringBuilder();
+ for (final UUID cur : b) {
+ for (final SubscriptionBundle bt : bundles) {
+ if (bt.getId().equals(cur)) {
+ if (!first) {
+ tmp.append(",");
+ }
+ tmp.append(bt.getExternalKey());
+ first = false;
+ break;
+ }
+ }
+ }
+ return tmp.toString();
+ }
+
+ public AccountTimelineJson(final Account account, final List<Invoice> invoices, final List<Payment> payments,
+ final List<SubscriptionBundle> bundles, final Multimap<UUID, Refund> refundsByPayment,
+ final Multimap<UUID, InvoicePayment> chargebacksByPayment, final AccountAuditLogs accountAuditLogs) {
+ this.account = new AccountJson(account, null, null, accountAuditLogs);
+ this.bundles = new LinkedList<BundleJson>();
+ for (final SubscriptionBundle bundle : bundles) {
+ final List<AuditLog> bundleAuditLogs = accountAuditLogs.getAuditLogsForBundle(bundle.getId());
+ final BundleJson jsonWithSubscriptions = new BundleJson(bundle, accountAuditLogs);
+ this.bundles.add(jsonWithSubscriptions);
+ }
+
+ this.invoices = new LinkedList<InvoiceJson>();
+ // Extract the credits from the invoices first
+ final List<CreditJson> credits = new ArrayList<CreditJson>();
+ for (final Invoice invoice : invoices) {
+ for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+ if (InvoiceItemType.CREDIT_ADJ.equals(invoiceItem.getInvoiceItemType())) {
+ final List<AuditLog> auditLogs = accountAuditLogs.getAuditLogsForInvoiceItem(invoiceItem.getId());
+ credits.add(new CreditJson(invoice, invoiceItem, auditLogs));
+ }
+ }
+ }
+ // Create now the invoice json objects
+ for (final Invoice invoice : invoices) {
+ final List<AuditLog> auditLogs = accountAuditLogs.getAuditLogsForInvoice(invoice.getId());
+ this.invoices.add(new InvoiceJson(invoice,
+ getBundleExternalKey(invoice, bundles),
+ credits,
+ auditLogs));
+ }
+
+ this.payments = new LinkedList<PaymentJson>();
+ for (final Payment payment : payments) {
+ final List<RefundJson> refunds = new ArrayList<RefundJson>();
+ for (final Refund refund : refundsByPayment.get(payment.getId())) {
+ final List<AuditLog> auditLogs = accountAuditLogs.getAuditLogsForRefund(refund.getId());
+ // TODO add adjusted invoice items?
+ refunds.add(new RefundJson(refund, null, auditLogs));
+ }
+
+ final List<ChargebackJson> chargebacks = new ArrayList<ChargebackJson>();
+ for (final InvoicePayment chargeback : chargebacksByPayment.get(payment.getId())) {
+ final List<AuditLog> auditLogs = accountAuditLogs.getAuditLogsForChargeback(chargeback.getId());
+ chargebacks.add(new ChargebackJson(payment.getAccountId(), chargeback, auditLogs));
+ }
+
+ final List<AuditLog> auditLogs = accountAuditLogs.getAuditLogsForPayment(payment.getId());
+ this.payments.add(new PaymentJson(payment,
+ getBundleExternalKey(payment.getInvoiceId(), invoices, bundles),
+ refunds,
+ chargebacks,
+ auditLogs));
+ }
+ }
+
+ public AccountJson getAccount() {
+ return account;
+ }
+
+ public List<BundleJson> getBundles() {
+ return bundles;
+ }
+
+ public List<InvoiceJson> getInvoices() {
+ return invoices;
+ }
+
+ public List<PaymentJson> getPayments() {
+ return payments;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("AccountTimelineJson");
+ sb.append("{account=").append(account);
+ sb.append(", bundles=").append(bundles);
+ sb.append(", invoices=").append(invoices);
+ sb.append(", payments=").append(payments);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final AccountTimelineJson that = (AccountTimelineJson) o;
+
+ if (account != null ? !account.equals(that.account) : that.account != null) {
+ return false;
+ }
+ if (bundles != null ? !bundles.equals(that.bundles) : that.bundles != null) {
+ return false;
+ }
+ if (invoices != null ? !invoices.equals(that.invoices) : that.invoices != null) {
+ return false;
+ }
+ if (payments != null ? !payments.equals(that.payments) : that.payments != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = account != null ? account.hashCode() : 0;
+ result = 31 * result + (bundles != null ? bundles.hashCode() : 0);
+ result = 31 * result + (invoices != null ? invoices.hashCode() : 0);
+ result = 31 * result + (payments != null ? payments.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AuditLogJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AuditLogJson.java
new file mode 100644
index 0000000..9db5376
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/AuditLogJson.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class AuditLogJson {
+
+ private final String changeType;
+ private final DateTime changeDate;
+ private final String changedBy;
+ private final String reasonCode;
+ private final String comments;
+ private final String userToken;
+
+ @JsonCreator
+ public AuditLogJson(@JsonProperty("changeType") final String changeType,
+ @JsonProperty("changeDate") final DateTime changeDate,
+ @JsonProperty("changedBy") final String changedBy,
+ @JsonProperty("reasonCode") final String reasonCode,
+ @JsonProperty("comments") final String comments,
+ @JsonProperty("userToken") final String userToken) {
+ this.changeType = changeType;
+ this.changeDate = changeDate;
+ this.changedBy = changedBy;
+ this.reasonCode = reasonCode;
+ this.comments = comments;
+ this.userToken = userToken;
+ }
+
+ public AuditLogJson(final AuditLog auditLog) {
+ this(auditLog.getChangeType().toString(), auditLog.getCreatedDate(), auditLog.getUserName(), auditLog.getReasonCode(),
+ auditLog.getComment(), auditLog.getUserToken());
+ }
+
+ public String getChangeType() {
+ return changeType;
+ }
+
+ public DateTime getChangeDate() {
+ return changeDate;
+ }
+
+ public String getChangedBy() {
+ return changedBy;
+ }
+
+ public String getReasonCode() {
+ return reasonCode;
+ }
+
+ public String getComments() {
+ return comments;
+ }
+
+ public String getUserToken() {
+ return userToken;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("AuditLogJson");
+ sb.append("{changeType='").append(changeType).append('\'');
+ sb.append(", changeDate=").append(changeDate);
+ sb.append(", changedBy=").append(changedBy);
+ sb.append(", reasonCode='").append(reasonCode).append('\'');
+ sb.append(", comments='").append(comments).append('\'');
+ sb.append(", userToken='").append(userToken).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final AuditLogJson that = (AuditLogJson) o;
+
+ if (changeDate != null ? changeDate.compareTo(that.changeDate) != 0 : that.changeDate != null) {
+ return false;
+ }
+ if (changeType != null ? !changeType.equals(that.changeType) : that.changeType != null) {
+ return false;
+ }
+ if (changedBy != null ? !changedBy.equals(that.changedBy) : that.changedBy != null) {
+ return false;
+ }
+ if (comments != null ? !comments.equals(that.comments) : that.comments != null) {
+ return false;
+ }
+ if (reasonCode != null ? !reasonCode.equals(that.reasonCode) : that.reasonCode != null) {
+ return false;
+ }
+ if (userToken != null ? !userToken.equals(that.userToken) : that.userToken != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = changeType != null ? changeType.hashCode() : 0;
+ result = 31 * result + (changeDate != null ? changeDate.hashCode() : 0);
+ result = 31 * result + (changedBy != null ? changedBy.hashCode() : 0);
+ result = 31 * result + (reasonCode != null ? reasonCode.hashCode() : 0);
+ result = 31 * result + (comments != null ? comments.hashCode() : 0);
+ result = 31 * result + (userToken != null ? userToken.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BillingExceptionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BillingExceptionJson.java
new file mode 100644
index 0000000..a009eb5
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BillingExceptionJson.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.BillingExceptionBase;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+// Doesn't extend JsonBase (no audit logs)
+public class BillingExceptionJson {
+
+ private final String className;
+ private final Integer code;
+ private final String message;
+ private final String causeClassName;
+ private final String causeMessage;
+ private final List<StackTraceElementJson> stackTrace;
+ // TODO add getSuppressed() from 1.7?
+
+ @JsonCreator
+ public BillingExceptionJson(@JsonProperty("className") final String className,
+ @JsonProperty("code") @Nullable final Integer code,
+ @JsonProperty("message") final String message,
+ @JsonProperty("causeClassName") final String causeClassName,
+ @JsonProperty("causeMessage") final String causeMessage,
+ @JsonProperty("stackTrace") final List<StackTraceElementJson> stackTrace) {
+ this.className = className;
+ this.code = code;
+ this.message = message;
+ this.causeClassName = causeClassName;
+ this.causeMessage = causeMessage;
+ this.stackTrace = stackTrace;
+ }
+
+ public BillingExceptionJson(final Exception exception) {
+ this(exception.getClass().getName(),
+ exception instanceof BillingExceptionBase ? ((BillingExceptionBase) exception).getCode() : null,
+ exception.getLocalizedMessage(),
+ exception.getCause() == null ? null : exception.getCause().getClass().getName(),
+ exception.getCause() == null ? null : exception.getCause().getLocalizedMessage(),
+ Lists.<StackTraceElement, StackTraceElementJson>transform(ImmutableList.<StackTraceElement>copyOf(exception.getStackTrace()),
+ new Function<StackTraceElement, StackTraceElementJson>() {
+ @Override
+ public StackTraceElementJson apply(final StackTraceElement input) {
+ return new StackTraceElementJson(input.getClassName(),
+ input.getFileName(),
+ input.getLineNumber(),
+ input.getMethodName(),
+ input.isNativeMethod());
+ }
+ }));
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public String getCauseClassName() {
+ return causeClassName;
+ }
+
+ public String getCauseMessage() {
+ return causeMessage;
+ }
+
+ public List<StackTraceElementJson> getStackTrace() {
+ return stackTrace;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("BillingExceptionJson{");
+ sb.append("className='").append(className).append('\'');
+ sb.append(", code=").append(code);
+ sb.append(", message='").append(message).append('\'');
+ sb.append(", causeClassName='").append(causeClassName).append('\'');
+ sb.append(", causeMessage='").append(causeMessage).append('\'');
+ sb.append(", stackTrace='").append(stackTrace).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final BillingExceptionJson that = (BillingExceptionJson) o;
+
+ if (causeClassName != null ? !causeClassName.equals(that.causeClassName) : that.causeClassName != null) {
+ return false;
+ }
+ if (causeMessage != null ? !causeMessage.equals(that.causeMessage) : that.causeMessage != null) {
+ return false;
+ }
+ if (className != null ? !className.equals(that.className) : that.className != null) {
+ return false;
+ }
+ if (code != null ? !code.equals(that.code) : that.code != null) {
+ return false;
+ }
+ if (message != null ? !message.equals(that.message) : that.message != null) {
+ return false;
+ }
+ if (stackTrace != null ? !stackTrace.equals(that.stackTrace) : that.stackTrace != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = className != null ? className.hashCode() : 0;
+ result = 31 * result + (code != null ? code.hashCode() : 0);
+ result = 31 * result + (message != null ? message.hashCode() : 0);
+ result = 31 * result + (causeClassName != null ? causeClassName.hashCode() : 0);
+ result = 31 * result + (causeMessage != null ? causeMessage.hashCode() : 0);
+ result = 31 * result + (stackTrace != null ? stackTrace.hashCode() : 0);
+ return result;
+ }
+
+ public static final class StackTraceElementJson {
+
+ private final String className;
+ private final String fileName;
+ private final Integer lineNumber;
+ private final String methodName;
+ private final Boolean nativeMethod;
+
+ @JsonCreator
+ public StackTraceElementJson(@JsonProperty("className") final String className,
+ @JsonProperty("fileName") final String fileName,
+ @JsonProperty("lineNumber") final Integer lineNumber,
+ @JsonProperty("methodName") final String methodName,
+ @JsonProperty("nativeMethod") final Boolean nativeMethod) {
+ this.className = className;
+ this.fileName = fileName;
+ this.lineNumber = lineNumber;
+ this.methodName = methodName;
+ this.nativeMethod = nativeMethod;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public Integer getLineNumber() {
+ return lineNumber;
+ }
+
+ public String getMethodName() {
+ return methodName;
+ }
+
+ public Boolean getNativeMethod() {
+ return nativeMethod;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("StackTraceElementJson{");
+ sb.append("className='").append(className).append('\'');
+ sb.append(", fileName='").append(fileName).append('\'');
+ sb.append(", lineNumber=").append(lineNumber);
+ sb.append(", methodName='").append(methodName).append('\'');
+ sb.append(", nativeMethod=").append(nativeMethod);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final StackTraceElementJson that = (StackTraceElementJson) o;
+
+ if (className != null ? !className.equals(that.className) : that.className != null) {
+ return false;
+ }
+ if (fileName != null ? !fileName.equals(that.fileName) : that.fileName != null) {
+ return false;
+ }
+ if (lineNumber != null ? !lineNumber.equals(that.lineNumber) : that.lineNumber != null) {
+ return false;
+ }
+ if (methodName != null ? !methodName.equals(that.methodName) : that.methodName != null) {
+ return false;
+ }
+ if (nativeMethod != null ? !nativeMethod.equals(that.nativeMethod) : that.nativeMethod != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = className != null ? className.hashCode() : 0;
+ result = 31 * result + (fileName != null ? fileName.hashCode() : 0);
+ result = 31 * result + (lineNumber != null ? lineNumber.hashCode() : 0);
+ result = 31 * result + (methodName != null ? methodName.hashCode() : 0);
+ result = 31 * result + (nativeMethod != null ? nativeMethod.hashCode() : 0);
+ return result;
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
new file mode 100644
index 0000000..249e149
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.entitlement.api.Subscription;
+import org.killbill.billing.entitlement.api.SubscriptionBundle;
+import org.killbill.billing.entitlement.api.SubscriptionEvent;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+public class BundleJson extends JsonBase {
+
+ protected final String accountId;
+ protected final String bundleId;
+ protected final String externalKey;
+ private final List<SubscriptionJson> subscriptions;
+
+ @JsonCreator
+ public BundleJson(@JsonProperty("accountId") @Nullable final String accountId,
+ @JsonProperty("bundleId") @Nullable final String bundleId,
+ @JsonProperty("externalKey") @Nullable final String externalKey,
+ @JsonProperty("subscriptions") @Nullable final List<SubscriptionJson> subscriptions,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.accountId = accountId;
+ this.bundleId = bundleId;
+ this.externalKey = externalKey;
+ this.subscriptions = subscriptions;
+ }
+
+ @JsonProperty("subscriptions")
+ public List<SubscriptionJson> getSubscriptions() {
+ return subscriptions;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public String getBundleId() {
+ return bundleId;
+ }
+
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ public BundleJson(final SubscriptionBundle bundle, @Nullable final AccountAuditLogs accountAuditLogs) {
+ super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForBundle(bundle.getId())));
+ this.accountId = bundle.getAccountId().toString();
+ this.bundleId = bundle.getId().toString();
+ this.externalKey = bundle.getExternalKey();
+
+ this.subscriptions = new LinkedList<SubscriptionJson>();
+ for (final Subscription cur : bundle.getSubscriptions()) {
+ final ImmutableList<SubscriptionEvent> events = ImmutableList.<SubscriptionEvent>copyOf(Collections2.filter(bundle.getTimeline().getSubscriptionEvents(), new Predicate<SubscriptionEvent>() {
+ @Override
+ public boolean apply(@Nullable final SubscriptionEvent input) {
+ return input.getEntitlementId().equals(cur.getId());
+ }
+ }));
+ this.subscriptions.add(new SubscriptionJson(cur, events, accountAuditLogs));
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "BundleJson{" +
+ "accountId='" + accountId + '\'' +
+ ", bundleId='" + bundleId + '\'' +
+ ", externalKey='" + externalKey + '\'' +
+ ", subscriptions=" + subscriptions +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final BundleJson that = (BundleJson) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
+ return false;
+ }
+ if (subscriptions != null ? !subscriptions.equals(that.subscriptions) : that.subscriptions != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = accountId != null ? accountId.hashCode() : 0;
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+ result = 31 * result + (subscriptions != null ? subscriptions.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java
new file mode 100644
index 0000000..ff0d8af
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class BundleTimelineJson {
+
+ private final String viewId;
+
+ private final BundleJson bundle;
+
+ private final List<PaymentJson> payments;
+
+ private final List<InvoiceJson> invoices;
+
+
+ private final String reasonForChange;
+
+ @JsonCreator
+ public BundleTimelineJson(@JsonProperty("viewId") final String viewId,
+ @JsonProperty("bundle") final BundleJson bundle,
+ @JsonProperty("payments") final List<PaymentJson> payments,
+ @JsonProperty("invoices") final List<InvoiceJson> invoices,
+ @JsonProperty("reasonForChange") final String reason) {
+ this.viewId = viewId;
+ this.bundle = bundle;
+ this.payments = payments;
+ this.invoices = invoices;
+ this.reasonForChange = reason;
+ }
+
+ public String getViewId() {
+ return viewId;
+ }
+
+ public BundleJson getBundle() {
+ return bundle;
+ }
+
+ public List<PaymentJson> getPayments() {
+ return payments;
+ }
+
+ public List<InvoiceJson> getInvoices() {
+ return invoices;
+ }
+
+ public String getReasonForChange() {
+ return reasonForChange;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final BundleTimelineJson that = (BundleTimelineJson) o;
+
+ if (bundle != null ? !bundle.equals(that.bundle) : that.bundle != null) {
+ return false;
+ }
+ if (invoices != null ? !invoices.equals(that.invoices) : that.invoices != null) {
+ return false;
+ }
+ if (payments != null ? !payments.equals(that.payments) : that.payments != null) {
+ return false;
+ }
+ if (reasonForChange != null ? !reasonForChange.equals(that.reasonForChange) : that.reasonForChange != null) {
+ return false;
+ }
+ if (viewId != null ? !viewId.equals(that.viewId) : that.viewId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = viewId != null ? viewId.hashCode() : 0;
+ result = 31 * result + (bundle != null ? bundle.hashCode() : 0);
+ result = 31 * result + (payments != null ? payments.hashCode() : 0);
+ result = 31 * result + (invoices != null ? invoices.hashCode() : 0);
+ result = 31 * result + (reasonForChange != null ? reasonForChange.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJsonSimple.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJsonSimple.java
new file mode 100644
index 0000000..3e63851
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJsonSimple.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CurrencyValueNull;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.Price;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.StaticCatalog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+public class CatalogJsonSimple {
+
+ private final String name;
+ private final List<ProductJson> products;
+
+ @JsonCreator
+ public CatalogJsonSimple(@JsonProperty("name") final String name,
+ @JsonProperty("products") final List<ProductJson> products) {
+ this.name = name;
+ this.products = products;
+ }
+
+
+ public CatalogJsonSimple(final StaticCatalog catalog) throws CatalogApiException {
+ name = catalog.getCatalogName();
+
+ final Plan[] plans = catalog.getCurrentPlans();
+ final Map<String, ProductJson> productMap = new HashMap<String, ProductJson>();
+ for (final Plan plan : plans) {
+ // Build the product associated with this plan
+ final Product product = plan.getProduct();
+ ProductJson productJson = productMap.get(product.getName());
+ if (productJson == null) {
+ productJson = new ProductJson(product.getCategory().toString(),
+ product.getName(),
+ toProductNames(product.getIncluded()),
+ toProductNames(product.getAvailable()));
+ productMap.put(product.getName(), productJson);
+ }
+
+ // Build the phases associated with this plan
+ final List<PhaseJson> phases = new LinkedList<PhaseJson>();
+ for (final PlanPhase phase : plan.getAllPhases()) {
+ final List<PriceJson> prices = new LinkedList<PriceJson>();
+ if (phase.getRecurringPrice() != null) {
+ for (final Price price : phase.getRecurringPrice().getPrices()) {
+ prices.add(new PriceJson(price));
+ }
+ }
+
+ final PhaseJson phaseJson = new PhaseJson(phase.getPhaseType().toString(), prices);
+ phases.add(phaseJson);
+ }
+
+ final PlanJson planJson = new PlanJson(plan.getName(), phases);
+ productJson.getPlans().add(planJson);
+ }
+
+ products = ImmutableList.<ProductJson>copyOf(productMap.values());
+ }
+
+ private List<String> toProductNames(final Product[] in) {
+ return Lists.transform(ImmutableList.<Product>copyOf(in),
+ new Function<Product, String>() {
+ @Override
+ public String apply(final Product input) {
+ return input.getName();
+ }
+ });
+ }
+
+ public List<ProductJson> getProducts() {
+ return products;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("CatalogJsonSimple{");
+ sb.append("name='").append(name).append('\'');
+ sb.append(", products=").append(products);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final CatalogJsonSimple that = (CatalogJsonSimple) o;
+
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+ if (products != null ? !products.equals(that.products) : that.products != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (products != null ? products.hashCode() : 0);
+ return result;
+ }
+
+ public static class ProductJson {
+
+ private final String type;
+ private final String name;
+ private final List<PlanJson> plans;
+ private final List<String> included;
+ private final List<String> available;
+
+ @JsonCreator
+ public ProductJson(@JsonProperty("type") final String type,
+ @JsonProperty("name") final String name,
+ @JsonProperty("plans") final List<PlanJson> plans,
+ @JsonProperty("included") final List<String> included,
+ @JsonProperty("available") final List<String> available) {
+ this.type = type;
+ this.name = name;
+ this.plans = plans;
+ this.included = included;
+ this.available = available;
+ }
+
+ public ProductJson(final String type, final String name, final List<String> included, final List<String> available) {
+ this(type, name, new LinkedList<PlanJson>(), included, available);
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List<PlanJson> getPlans() {
+ return plans;
+ }
+
+ public List<String> getIncluded() {
+ return included;
+ }
+
+ public List<String> getAvailable() {
+ return available;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ProductJson{");
+ sb.append("type='").append(type).append('\'');
+ sb.append(", name='").append(name).append('\'');
+ sb.append(", plans=").append(plans);
+ sb.append(", included=").append(included);
+ sb.append(", available=").append(available);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final ProductJson that = (ProductJson) o;
+
+ if (available != null ? !available.equals(that.available) : that.available != null) {
+ return false;
+ }
+ if (included != null ? !included.equals(that.included) : that.included != null) {
+ return false;
+ }
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+ if (plans != null ? !plans.equals(that.plans) : that.plans != null) {
+ return false;
+ }
+ if (type != null ? !type.equals(that.type) : that.type != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = type != null ? type.hashCode() : 0;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (plans != null ? plans.hashCode() : 0);
+ result = 31 * result + (included != null ? included.hashCode() : 0);
+ result = 31 * result + (available != null ? available.hashCode() : 0);
+ return result;
+ }
+ }
+
+ public static class PlanJson {
+
+ private final String name;
+ private final List<PhaseJson> phases;
+
+ @JsonCreator
+ public PlanJson(@JsonProperty("name") final String name,
+ @JsonProperty("phases") final List<PhaseJson> phases) {
+ this.name = name;
+ this.phases = phases;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List<PhaseJson> getPhases() {
+ return phases;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("PlanJson{");
+ sb.append("name='").append(name).append('\'');
+ sb.append(", phases=").append(phases);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final PlanJson planJson = (PlanJson) o;
+
+ if (name != null ? !name.equals(planJson.name) : planJson.name != null) {
+ return false;
+ }
+ if (phases != null ? !phases.equals(planJson.phases) : planJson.phases != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (phases != null ? phases.hashCode() : 0);
+ return result;
+ }
+ }
+
+ public static class PhaseJson {
+
+ private final String type;
+ private final List<PriceJson> prices;
+
+ @JsonCreator
+ public PhaseJson(@JsonProperty("type") final String type,
+ @JsonProperty("prices") final List<PriceJson> prices) {
+ this.type = type;
+ this.prices = prices;
+ }
+
+ public String getType() {
+ return type;
+ }
+ public List<PriceJson> getPrices() {
+ return prices;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("PhaseJson{");
+ sb.append("type='").append(type).append('\'');
+ sb.append(", prices=").append(prices);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final PhaseJson phaseJson = (PhaseJson) o;
+
+ if (prices != null ? !prices.equals(phaseJson.prices) : phaseJson.prices != null) {
+ return false;
+ }
+ if (type != null ? !type.equals(phaseJson.type) : phaseJson.type != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = type != null ? type.hashCode() : 0;
+ result = 31 * result + (prices != null ? prices.hashCode() : 0);
+ return result;
+ }
+ }
+
+ public static class PriceJson {
+
+ private final String currency;
+ private final BigDecimal value;
+
+ @JsonCreator
+ public PriceJson(@JsonProperty("currency") final String currency,
+ @JsonProperty("value") final BigDecimal value) {
+ this.currency = currency;
+ this.value = value;
+ }
+
+ public PriceJson(final Price price) throws CurrencyValueNull {
+ this(price.getCurrency().toString(), price.getValue());
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public BigDecimal getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("PriceJson{");
+ sb.append("currency='").append(currency).append('\'');
+ sb.append(", value=").append(value);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final PriceJson priceJson = (PriceJson) o;
+
+ if (currency != null ? !currency.equals(priceJson.currency) : priceJson.currency != null) {
+ return false;
+ }
+ if (value != null ? !value.equals(priceJson.value) : priceJson.value != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = currency != null ? currency.hashCode() : 0;
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ return result;
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ChargebackJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ChargebackJson.java
new file mode 100644
index 0000000..ee42e75
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/ChargebackJson.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class ChargebackJson extends JsonBase {
+
+ private final String chargebackId;
+ private final String accountId;
+ private final DateTime requestedDate;
+ private final DateTime effectiveDate;
+ private final BigDecimal amount;
+ private final String paymentId;
+ private final String currency;
+
+ @JsonCreator
+ public ChargebackJson(@JsonProperty("chargebackId") final String chargebackId,
+ @JsonProperty("accountId") final String accountId,
+ @JsonProperty("requestedDate") final DateTime requestedDate,
+ @JsonProperty("effectiveDate") final DateTime effectiveDate,
+ @JsonProperty("amount") final BigDecimal chargebackAmount,
+ @JsonProperty("paymentId") final String paymentId,
+ @JsonProperty("currency") final String currency,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.chargebackId = chargebackId;
+ this.accountId = accountId;
+ this.requestedDate = requestedDate;
+ this.effectiveDate = effectiveDate;
+ this.amount = chargebackAmount;
+ this.paymentId = paymentId;
+ this.currency = currency;
+ }
+
+ public ChargebackJson(final UUID accountId, final InvoicePayment chargeback) {
+ this(accountId, chargeback, null);
+ }
+
+ public ChargebackJson(final UUID accountId, final InvoicePayment chargeback, @Nullable final List<AuditLog> auditLogs) {
+ this(chargeback.getId().toString(), accountId.toString(), chargeback.getPaymentDate(), chargeback.getPaymentDate(),
+ chargeback.getAmount().negate(), chargeback.getPaymentId().toString(), chargeback.getCurrency().toString(), toAuditLogJson(auditLogs));
+ }
+
+ public String getChargebackId() {
+ return chargebackId;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public DateTime getRequestedDate() {
+ return requestedDate;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public String getPaymentId() {
+ return paymentId;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final ChargebackJson that = (ChargebackJson) o;
+
+ if (chargebackId != null ? !chargebackId.equals(that.chargebackId) : that.chargebackId != null) {
+ return false;
+ }
+ if (!((amount == null && that.amount == null) ||
+ (amount != null && that.amount != null && amount.compareTo(that.amount) == 0))) {
+ return false;
+ }
+ if (!((effectiveDate == null && that.effectiveDate == null) ||
+ (effectiveDate != null && that.effectiveDate != null && effectiveDate.compareTo(that.effectiveDate) == 0))) {
+ return false;
+ }
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
+ return false;
+ }
+ if (currency != null ? !currency.equals(that.currency) : that.currency != null) {
+ return false;
+ }
+ if (!((requestedDate == null && that.requestedDate == null) ||
+ (requestedDate != null && that.requestedDate != null && requestedDate.compareTo(that.requestedDate) == 0))) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = chargebackId != null ? chargebackId.hashCode() : 0;
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (requestedDate != null ? requestedDate.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ChargebackJson{" +
+ "chargebackId='" + chargebackId + '\'' +
+ ", accountId='" + accountId + '\'' +
+ ", requestedDate=" + requestedDate +
+ ", effectiveDate=" + effectiveDate +
+ ", amount=" + amount +
+ ", paymentId='" + paymentId + '\'' +
+ ", currency='" + currency + '\'' +
+ '}';
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CreditJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CreditJson.java
new file mode 100644
index 0000000..ab5112d
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CreditJson.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class CreditJson extends JsonBase {
+
+ private final BigDecimal creditAmount;
+ private final String invoiceId;
+ private final String invoiceNumber;
+ private final LocalDate effectiveDate;
+ private final String accountId;
+
+ @JsonCreator
+ public CreditJson(@JsonProperty("creditAmount") final BigDecimal creditAmount,
+ @JsonProperty("invoiceId") final String invoiceId,
+ @JsonProperty("invoiceNumber") final String invoiceNumber,
+ @JsonProperty("effectiveDate") final LocalDate effectiveDate,
+ @JsonProperty("accountId") final String accountId,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.creditAmount = creditAmount;
+ this.invoiceId = invoiceId;
+ this.invoiceNumber = invoiceNumber;
+ this.effectiveDate = effectiveDate;
+ this.accountId = accountId;
+ }
+
+ public CreditJson(final Invoice invoice, final InvoiceItem credit, final List<AuditLog> auditLogs) {
+ super(toAuditLogJson(auditLogs));
+ this.accountId = toString(credit.getAccountId());
+ this.creditAmount = credit.getAmount();
+ this.invoiceId = toString(credit.getInvoiceId());
+ this.invoiceNumber = invoice.getInvoiceNumber().toString();
+ this.effectiveDate = credit.getStartDate();
+ }
+
+ public CreditJson(final Invoice invoice, final InvoiceItem credit) {
+ this(invoice, credit, null);
+ }
+
+ public BigDecimal getCreditAmount() {
+ return creditAmount;
+ }
+
+ public String getInvoiceId() {
+ return invoiceId;
+ }
+
+ public String getInvoiceNumber() {
+ return invoiceNumber;
+ }
+
+ public LocalDate getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("CreditJson");
+ sb.append("{creditAmount=").append(creditAmount);
+ sb.append(", invoiceId=").append(invoiceId);
+ sb.append(", invoiceNumber='").append(invoiceNumber).append('\'');
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", accountId=").append(accountId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final CreditJson that = (CreditJson) o;
+
+ if (!((creditAmount == null && that.creditAmount == null) ||
+ (creditAmount != null && that.creditAmount != null && creditAmount.compareTo(that.creditAmount) == 0))) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (invoiceNumber != null ? !invoiceNumber.equals(that.invoiceNumber) : that.invoiceNumber != null) {
+ return false;
+ }
+ if (!((effectiveDate == null && that.effectiveDate == null) ||
+ (effectiveDate != null && that.effectiveDate != null && effectiveDate.compareTo(that.effectiveDate) == 0))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = creditAmount != null ? creditAmount.hashCode() : 0;
+ result = 31 * result + (invoiceId != null ? invoiceId.hashCode() : 0);
+ result = 31 * result + (invoiceNumber != null ? invoiceNumber.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CustomFieldJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CustomFieldJson.java
new file mode 100644
index 0000000..c0abc8a
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CustomFieldJson.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.customfield.CustomField;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class CustomFieldJson extends JsonBase {
+
+ private final String customFieldId;
+ private final String objectId;
+ private final ObjectType objectType;
+ private final String name;
+ private final String value;
+
+ @JsonCreator
+ public CustomFieldJson(@JsonProperty("customFieldId") final String customFieldId,
+ @JsonProperty("objectId") final String objectId,
+ @JsonProperty("objectType") final ObjectType objectType,
+ @JsonProperty("name") @Nullable final String name,
+ @JsonProperty("value") @Nullable final String value,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.customFieldId = customFieldId;
+ this.objectId = objectId;
+ this.objectType = objectType;
+ this.name = name;
+ this.value = value;
+ }
+
+ public CustomFieldJson(final CustomField input, @Nullable final List<AuditLog> auditLogs) {
+ this(input.getId().toString(), input.getObjectId().toString(), input.getObjectType(), input.getFieldName(), input.getFieldValue(), toAuditLogJson(auditLogs));
+ }
+
+ public String getCustomFieldId() {
+ return customFieldId;
+ }
+
+ public String getObjectId() {
+ return objectId;
+ }
+
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("CustomFieldJson{");
+ sb.append("customFieldId='").append(customFieldId).append('\'');
+ sb.append(", objectId=").append(objectId);
+ sb.append(", objectType=").append(objectType);
+ sb.append(", name='").append(name).append('\'');
+ sb.append(", value='").append(value).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final CustomFieldJson that = (CustomFieldJson) o;
+
+ if (customFieldId != null ? !customFieldId.equals(that.customFieldId) : that.customFieldId != null) {
+ return false;
+ }
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+ if (objectId != null ? !objectId.equals(that.objectId) : that.objectId != null) {
+ return false;
+ }
+ if (objectType != that.objectType) {
+ return false;
+ }
+ if (value != null ? !value.equals(that.value) : that.value != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = customFieldId != null ? customFieldId.hashCode() : 0;
+ result = 31 * result + (objectId != null ? objectId.hashCode() : 0);
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceEmailJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceEmailJson.java
new file mode 100644
index 0000000..0a4f024
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceEmailJson.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class InvoiceEmailJson extends JsonBase {
+
+ private final String accountId;
+ private final boolean isNotifiedForInvoices;
+
+ @JsonCreator
+ public InvoiceEmailJson(@JsonProperty("accountId") final String accountId,
+ @JsonProperty("isNotifiedForInvoices") final boolean isNotifiedForInvoices) {
+ this.accountId = accountId;
+ this.isNotifiedForInvoices = isNotifiedForInvoices;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ @JsonGetter("isNotifiedForInvoices")
+ public boolean isNotifiedForInvoices() {
+ return isNotifiedForInvoices;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("InvoiceEmailJson");
+ sb.append("{accountId='").append(accountId).append('\'');
+ sb.append(", isNotifiedForInvoices=").append(isNotifiedForInvoices);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final InvoiceEmailJson that = (InvoiceEmailJson) o;
+
+ if (isNotifiedForInvoices != that.isNotifiedForInvoices) {
+ return false;
+ }
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = accountId != null ? accountId.hashCode() : 0;
+ result = 31 * result + (isNotifiedForInvoices ? 1 : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceItemJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceItemJson.java
new file mode 100644
index 0000000..ab424d5
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceItemJson.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class InvoiceItemJson extends JsonBase {
+
+ private final String invoiceItemId;
+ private final String invoiceId;
+ private final String linkedInvoiceItemId;
+ private final String accountId;
+ private final String bundleId;
+ private final String subscriptionId;
+ private final String planName;
+ private final String phaseName;
+ private final String itemType;
+ private final String description;
+ private final LocalDate startDate;
+ private final LocalDate endDate;
+ private final BigDecimal amount;
+ private final Currency currency;
+
+ @JsonCreator
+ public InvoiceItemJson(@JsonProperty("invoiceItemId") final String invoiceItemId,
+ @JsonProperty("invoiceId") final String invoiceId,
+ @JsonProperty("linkedInvoiceItemId") final String linkedInvoiceItemId,
+ @JsonProperty("accountId") final String accountId,
+ @JsonProperty("bundleId") final String bundleId,
+ @JsonProperty("subscriptionId") final String subscriptionId,
+ @JsonProperty("planName") final String planName,
+ @JsonProperty("phaseName") final String phaseName,
+ @JsonProperty("itemType") final String itemType,
+ @JsonProperty("description") final String description,
+ @JsonProperty("startDate") final LocalDate startDate,
+ @JsonProperty("endDate") final LocalDate endDate,
+ @JsonProperty("amount") final BigDecimal amount,
+ @JsonProperty("currency") final Currency currency,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.invoiceItemId = invoiceItemId;
+ this.invoiceId = invoiceId;
+ this.linkedInvoiceItemId = linkedInvoiceItemId;
+ this.accountId = accountId;
+ this.bundleId = bundleId;
+ this.subscriptionId = subscriptionId;
+ this.planName = planName;
+ this.phaseName = phaseName;
+ this.itemType = itemType;
+ this.description = description;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.amount = amount;
+ this.currency = currency;
+ }
+
+ public InvoiceItemJson(final InvoiceItem item, @Nullable final List<AuditLog> auditLogs) {
+ this(toString(item.getId()), toString(item.getInvoiceId()), toString(item.getLinkedItemId()),
+ toString(item.getAccountId()), toString(item.getBundleId()), toString(item.getSubscriptionId()),
+ item.getPlanName(), item.getPhaseName(), item.getInvoiceItemType().toString(),
+ item.getDescription(), item.getStartDate(), item.getEndDate(),
+ item.getAmount(), item.getCurrency(), toAuditLogJson(auditLogs));
+ }
+
+ public InvoiceItemJson(final InvoiceItem input) {
+ this(input, null);
+ }
+
+ public String getInvoiceItemId() {
+ return invoiceItemId;
+ }
+
+ public String getInvoiceId() {
+ return invoiceId;
+ }
+
+ public String getLinkedInvoiceItemId() {
+ return linkedInvoiceItemId;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public String getBundleId() {
+ return bundleId;
+ }
+
+ public String getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ public String getPlanName() {
+ return planName;
+ }
+
+ public String getPhaseName() {
+ return phaseName;
+ }
+
+ public String getItemType() {
+ return itemType;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+
+ public LocalDate getEndDate() {
+ return endDate;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("InvoiceItemJson");
+ sb.append("{invoiceItemId='").append(invoiceItemId).append('\'');
+ sb.append(", invoiceId='").append(invoiceId).append('\'');
+ sb.append(", linkedInvoiceItemId='").append(linkedInvoiceItemId).append('\'');
+ sb.append(", accountId='").append(accountId).append('\'');
+ sb.append(", bundleId='").append(bundleId).append('\'');
+ sb.append(", subscriptionId='").append(subscriptionId).append('\'');
+ sb.append(", planName='").append(planName).append('\'');
+ sb.append(", phaseName='").append(phaseName).append('\'');
+ sb.append(", description='").append(description).append('\'');
+ sb.append(", startDate=").append(startDate);
+ sb.append(", endDate=").append(endDate);
+ sb.append(", amount=").append(amount);
+ sb.append(", currency=").append(currency);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final InvoiceItemJson that = (InvoiceItemJson) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (!((amount == null && that.amount == null) ||
+ (amount != null && that.amount != null && amount.compareTo(that.amount) == 0))) {
+ return false;
+ }
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (description != null ? !description.equals(that.description) : that.description != null) {
+ return false;
+ }
+ if (!((endDate == null && that.endDate == null) ||
+ (endDate != null && that.endDate != null && endDate.compareTo(that.endDate) == 0))) {
+ return false;
+ }
+ if (invoiceItemId != null ? !invoiceItemId.equals(that.invoiceItemId) : that.invoiceItemId != null) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (linkedInvoiceItemId != null ? !linkedInvoiceItemId.equals(that.linkedInvoiceItemId) : that.linkedInvoiceItemId != null) {
+ return false;
+ }
+ if (phaseName != null ? !phaseName.equals(that.phaseName) : that.phaseName != null) {
+ return false;
+ }
+ if (planName != null ? !planName.equals(that.planName) : that.planName != null) {
+ return false;
+ }
+ if (!((startDate == null && that.startDate == null) ||
+ (startDate != null && that.startDate != null && startDate.compareTo(that.startDate) == 0))) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = invoiceId != null ? invoiceId.hashCode() : 0;
+ result = 31 * result + (invoiceItemId != null ? invoiceItemId.hashCode() : 0);
+ result = 31 * result + (linkedInvoiceItemId != null ? linkedInvoiceItemId.hashCode() : 0);
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (subscriptionId != null ? subscriptionId.hashCode() : 0);
+ result = 31 * result + (planName != null ? planName.hashCode() : 0);
+ result = 31 * result + (phaseName != null ? phaseName.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (startDate != null ? startDate.hashCode() : 0);
+ result = 31 * result + (endDate != null ? endDate.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ return result;
+ }
+}
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
new file mode 100644
index 0000000..b45e4c4
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/InvoiceJson.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+import org.killbill.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class InvoiceJson extends JsonBase {
+
+ private final BigDecimal amount;
+ private final String currency;
+ private final String invoiceId;
+ private final LocalDate invoiceDate;
+ private final LocalDate targetDate;
+ private final String invoiceNumber;
+ private final BigDecimal balance;
+ private final BigDecimal creditAdj;
+ private final BigDecimal refundAdj;
+ private final String accountId;
+ private final List<InvoiceItemJson> items;
+ private final String bundleKeys;
+ private final List<CreditJson> credits;
+
+ @JsonCreator
+ public InvoiceJson(@JsonProperty("amount") final BigDecimal amount,
+ @JsonProperty("currency") final String currency,
+ @JsonProperty("creditAdj") final BigDecimal creditAdj,
+ @JsonProperty("refundAdj") final BigDecimal refundAdj,
+ @JsonProperty("invoiceId") final String invoiceId,
+ @JsonProperty("invoiceDate") final LocalDate invoiceDate,
+ @JsonProperty("targetDate") final LocalDate targetDate,
+ @JsonProperty("invoiceNumber") final String invoiceNumber,
+ @JsonProperty("balance") final BigDecimal balance,
+ @JsonProperty("accountId") final String accountId,
+ @JsonProperty("externalBundleKeys") final String bundleKeys,
+ @JsonProperty("credits") final List<CreditJson> credits,
+ @JsonProperty("items") final List<InvoiceItemJson> items,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.amount = amount;
+ this.currency = currency;
+ this.creditAdj = creditAdj;
+ this.refundAdj = refundAdj;
+ this.invoiceId = invoiceId;
+ this.invoiceDate = invoiceDate;
+ this.targetDate = targetDate;
+ this.invoiceNumber = invoiceNumber;
+ this.balance = balance;
+ this.accountId = accountId;
+ this.bundleKeys = bundleKeys;
+ this.credits = credits;
+ this.items = items;
+ }
+
+ public InvoiceJson(final Invoice input) {
+ this(input, false, null);
+ }
+
+ public InvoiceJson(final Invoice input, final String bundleKeys, final List<CreditJson> credits, final List<AuditLog> auditLogs) {
+ this(input.getChargedAmount(), input.getCurrency().toString(), input.getCreditedAmount(), input.getRefundedAmount(),
+ input.getId().toString(), input.getInvoiceDate(), input.getTargetDate(), String.valueOf(input.getInvoiceNumber()),
+ input.getBalance(), input.getAccountId().toString(), bundleKeys, credits, null, toAuditLogJson(auditLogs));
+ }
+
+ public InvoiceJson(final Invoice input, final boolean withItems, @Nullable final AccountAuditLogs accountAuditLogs) {
+ super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForInvoice(input.getId())));
+ this.items = new ArrayList<InvoiceItemJson>(input.getInvoiceItems().size());
+ if (withItems) {
+ for (final InvoiceItem item : input.getInvoiceItems()) {
+ this.items.add(new InvoiceItemJson(item, accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForInvoiceItem(item.getId())));
+ }
+ }
+ this.amount = input.getChargedAmount();
+ this.currency = input.getCurrency().toString();
+ this.creditAdj = input.getCreditedAmount();
+ this.refundAdj = input.getRefundedAmount();
+ this.invoiceId = input.getId().toString();
+ this.invoiceDate = input.getInvoiceDate();
+ this.targetDate = input.getTargetDate();
+ this.invoiceNumber = input.getInvoiceNumber() == null ? null : String.valueOf(input.getInvoiceNumber());
+ this.balance = input.getBalance();
+ this.accountId = input.getAccountId().toString();
+ this.bundleKeys = null;
+ this.credits = null;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public String getInvoiceId() {
+ return invoiceId;
+ }
+
+ public LocalDate getInvoiceDate() {
+ return invoiceDate;
+ }
+
+ public LocalDate getTargetDate() {
+ return targetDate;
+ }
+
+ public String getInvoiceNumber() {
+ return invoiceNumber;
+ }
+
+ public BigDecimal getBalance() {
+ return balance;
+ }
+
+ public BigDecimal getCreditAdj() {
+ return creditAdj;
+ }
+
+ public BigDecimal getRefundAdj() {
+ return refundAdj;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public List<InvoiceItemJson> getItems() {
+ return items;
+ }
+
+ public String getBundleKeys() {
+ return bundleKeys;
+ }
+
+ public List<CreditJson> getCredits() {
+ return credits;
+ }
+
+ @Override
+ public String toString() {
+ return "InvoiceJson{" +
+ "amount=" + amount +
+ ", currency='" + currency + '\'' +
+ ", invoiceId='" + invoiceId + '\'' +
+ ", invoiceDate=" + invoiceDate +
+ ", targetDate=" + targetDate +
+ ", invoiceNumber='" + invoiceNumber + '\'' +
+ ", balance=" + balance +
+ ", creditAdj=" + creditAdj +
+ ", refundAdj=" + refundAdj +
+ ", accountId='" + accountId + '\'' +
+ ", items=" + items +
+ ", bundleKeys='" + bundleKeys + '\'' +
+ ", credits=" + credits +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final InvoiceJson that = (InvoiceJson) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (amount != null ? amount.compareTo(that.amount) != 0 : that.amount != null) {
+ return false;
+ }
+ if (balance != null ? balance.compareTo(that.balance) != 0 : that.balance != null) {
+ return false;
+ }
+ if (bundleKeys != null ? !bundleKeys.equals(that.bundleKeys) : that.bundleKeys != null) {
+ return false;
+ }
+ if (creditAdj != null ? creditAdj.compareTo(that.creditAdj) != 0 : that.creditAdj != null) {
+ return false;
+ }
+ if (credits != null ? !credits.equals(that.credits) : that.credits != null) {
+ return false;
+ }
+ if (currency != null ? !currency.equals(that.currency) : that.currency != null) {
+ return false;
+ }
+ if (invoiceDate != null ? invoiceDate.compareTo(that.invoiceDate) != 0 : that.invoiceDate != null) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (invoiceNumber != null ? !invoiceNumber.equals(that.invoiceNumber) : that.invoiceNumber != null) {
+ return false;
+ }
+ if (items != null ? !items.equals(that.items) : that.items != null) {
+ return false;
+ }
+ if (refundAdj != null ? refundAdj.compareTo(that.refundAdj) != 0 : that.refundAdj != null) {
+ return false;
+ }
+ if (targetDate != null ? targetDate.compareTo(that.targetDate) != 0 : that.targetDate != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = amount != null ? amount.hashCode() : 0;
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (invoiceId != null ? invoiceId.hashCode() : 0);
+ result = 31 * result + (invoiceDate != null ? invoiceDate.hashCode() : 0);
+ result = 31 * result + (targetDate != null ? targetDate.hashCode() : 0);
+ result = 31 * result + (invoiceNumber != null ? invoiceNumber.hashCode() : 0);
+ result = 31 * result + (balance != null ? balance.hashCode() : 0);
+ result = 31 * result + (creditAdj != null ? creditAdj.hashCode() : 0);
+ result = 31 * result + (refundAdj != null ? refundAdj.hashCode() : 0);
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (items != null ? items.hashCode() : 0);
+ result = 31 * result + (bundleKeys != null ? bundleKeys.hashCode() : 0);
+ result = 31 * result + (credits != null ? credits.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/JsonBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/JsonBase.java
new file mode 100644
index 0000000..9ac1001
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/JsonBase.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.util.audit.AuditLog;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+public abstract class JsonBase {
+
+ protected List<AuditLogJson> auditLogs;
+
+ public JsonBase() {
+ this(null);
+ }
+
+ public JsonBase(@Nullable final List<AuditLogJson> auditLogs) {
+ this.auditLogs = auditLogs;
+ }
+
+ protected static ImmutableList<AuditLogJson> toAuditLogJson(@Nullable final List<AuditLog> auditLogs) {
+ if (auditLogs == null) {
+ return null;
+ }
+
+ return ImmutableList.<AuditLogJson>copyOf(Collections2.transform(auditLogs, new Function<AuditLog, AuditLogJson>() {
+ @Override
+ public AuditLogJson apply(@Nullable final AuditLog input) {
+ return new AuditLogJson(input);
+ }
+ }));
+ }
+
+ protected static String toString(@Nullable final UUID id) {
+ return id == null ? null : id.toString();
+ }
+
+ public List<AuditLogJson> getAuditLogs() {
+ return auditLogs;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/NotificationJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/NotificationJson.java
new file mode 100644
index 0000000..e8fcd14
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/NotificationJson.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import org.killbill.billing.notification.plugin.api.ExtBusEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/*
+ * Use to communicate back with client after they registered a callback
+ */
+public class NotificationJson {
+
+ private final String eventType;
+ private final String accountId;
+ private final String objectType;
+ private final String objectId;
+
+ @JsonCreator
+ public NotificationJson(@JsonProperty("eventType") final String eventType,
+ @JsonProperty("accountId") final String accountId,
+ @JsonProperty("objectType") final String objectType,
+ @JsonProperty("objectId") final String objectId) {
+ this.eventType = eventType;
+ this.accountId = accountId;
+ this.objectType = objectType;
+ this.objectId = objectId;
+ }
+
+ public NotificationJson(final ExtBusEvent event) {
+ this(event.getEventType().toString(),
+ event.getAccountId() != null ? event.getAccountId().toString() : null,
+ event.getObjectType().toString(),
+ event.getObjectId() != null ? event.getObjectId().toString() : null);
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public String getObjectType() {
+ return objectType;
+ }
+
+ public String getObjectId() {
+ return objectId;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateJson.java
new file mode 100644
index 0000000..ec76f2f
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateJson.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import org.joda.time.Period;
+
+import org.killbill.billing.overdue.OverdueApiException;
+import org.killbill.billing.overdue.OverdueState;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class OverdueStateJson {
+
+ private final String name;
+ private final String externalMessage;
+ private final Integer daysBetweenPaymentRetries;
+ private final Boolean disableEntitlementAndChangesBlocked;
+ private final Boolean blockChanges;
+ private final Boolean isClearState;
+ private final Integer reevaluationIntervalDays;
+
+ @JsonCreator
+ public OverdueStateJson(@JsonProperty("name") final String name,
+ @JsonProperty("externalMessage") final String externalMessage,
+ @JsonProperty("daysBetweenPaymentRetries") final Integer daysBetweenPaymentRetries,
+ @JsonProperty("disableEntitlementAndChangesBlocked") final Boolean disableEntitlementAndChangesBlocked,
+ @JsonProperty("blockChanges") final Boolean blockChanges,
+ @JsonProperty("clearState") final Boolean isClearState,
+ @JsonProperty("reevaluationIntervalDays") final Integer reevaluationIntervalDays) {
+ this.name = name;
+ this.externalMessage = externalMessage;
+ this.daysBetweenPaymentRetries = daysBetweenPaymentRetries;
+ this.disableEntitlementAndChangesBlocked = disableEntitlementAndChangesBlocked;
+ this.blockChanges = blockChanges;
+ this.isClearState = isClearState;
+ this.reevaluationIntervalDays = reevaluationIntervalDays;
+ }
+
+ public OverdueStateJson(final OverdueState overdueState) {
+ this.name = overdueState.getName();
+ this.externalMessage = overdueState.getExternalMessage();
+ this.daysBetweenPaymentRetries = overdueState.getDaysBetweenPaymentRetries();
+ this.disableEntitlementAndChangesBlocked = overdueState.disableEntitlementAndChangesBlocked();
+ this.blockChanges = overdueState.blockChanges();
+ this.isClearState = overdueState.isClearState();
+
+ Period reevaluationIntervalPeriod = null;
+ try {
+ reevaluationIntervalPeriod = overdueState.getReevaluationInterval();
+ } catch (OverdueApiException ignored) {
+ }
+
+ if (reevaluationIntervalPeriod != null) {
+ this.reevaluationIntervalDays = reevaluationIntervalPeriod.getDays();
+ } else {
+ this.reevaluationIntervalDays = null;
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getExternalMessage() {
+ return externalMessage;
+ }
+
+ public Integer getDaysBetweenPaymentRetries() {
+ return daysBetweenPaymentRetries;
+ }
+
+ public Boolean isDisableEntitlementAndChangesBlocked() {
+ return disableEntitlementAndChangesBlocked;
+ }
+
+ public Boolean isBlockChanges() {
+ return blockChanges;
+ }
+
+ public Boolean isClearState() {
+ return isClearState;
+ }
+
+ public Integer getReevaluationIntervalDays() {
+ return reevaluationIntervalDays;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("OverdueStateJson");
+ sb.append("{name='").append(name).append('\'');
+ sb.append(", externalMessage='").append(externalMessage).append('\'');
+ sb.append(", daysBetweenPaymentRetries=").append(daysBetweenPaymentRetries);
+ sb.append(", disableEntitlementAndChangesBlocked=").append(disableEntitlementAndChangesBlocked);
+ sb.append(", blockChanges=").append(blockChanges);
+ sb.append(", isClearState=").append(isClearState);
+ sb.append(", reevaluationIntervalDays=").append(reevaluationIntervalDays);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final OverdueStateJson that = (OverdueStateJson) o;
+
+ if (blockChanges != null ? !blockChanges.equals(that.blockChanges) : that.blockChanges != null) {
+ return false;
+ }
+ if (daysBetweenPaymentRetries != null ? !daysBetweenPaymentRetries.equals(that.daysBetweenPaymentRetries) : that.daysBetweenPaymentRetries != null) {
+ return false;
+ }
+ if (disableEntitlementAndChangesBlocked != null ? !disableEntitlementAndChangesBlocked.equals(that.disableEntitlementAndChangesBlocked) : that.disableEntitlementAndChangesBlocked != null) {
+ return false;
+ }
+ if (externalMessage != null ? !externalMessage.equals(that.externalMessage) : that.externalMessage != null) {
+ return false;
+ }
+ if (isClearState != null ? !isClearState.equals(that.isClearState) : that.isClearState != null) {
+ return false;
+ }
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+ if (reevaluationIntervalDays != null ? !reevaluationIntervalDays.equals(that.reevaluationIntervalDays) : that.reevaluationIntervalDays != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (externalMessage != null ? externalMessage.hashCode() : 0);
+ result = 31 * result + (daysBetweenPaymentRetries != null ? daysBetweenPaymentRetries.hashCode() : 0);
+ result = 31 * result + (disableEntitlementAndChangesBlocked != null ? disableEntitlementAndChangesBlocked.hashCode() : 0);
+ result = 31 * result + (blockChanges != null ? blockChanges.hashCode() : 0);
+ result = 31 * result + (isClearState != null ? isClearState.hashCode() : 0);
+ result = 31 * result + (reevaluationIntervalDays != null ? reevaluationIntervalDays.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentJson.java
new file mode 100644
index 0000000..8441ecb
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentJson.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.clock.DefaultClock;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PaymentJson extends JsonBase {
+
+ private final BigDecimal paidAmount;
+ private final BigDecimal amount;
+ private final String accountId;
+ private final String invoiceId;
+ private final String paymentId;
+ private final String paymentNumber;
+ private final DateTime requestedDate;
+ private final DateTime effectiveDate;
+ private final Integer retryCount;
+ private final String currency;
+ private final String status;
+ private final String gatewayErrorCode;
+ private final String gatewayErrorMsg;
+ private final String paymentMethodId;
+ private final String bundleKeys;
+ private final List<RefundJson> refunds;
+ private final List<ChargebackJson> chargebacks;
+
+ @JsonCreator
+ public PaymentJson(@JsonProperty("amount") final BigDecimal amount,
+ @JsonProperty("paidAmount") final BigDecimal paidAmount,
+ @JsonProperty("accountId") final String accountId,
+ @JsonProperty("invoiceId") final String invoiceId,
+ @JsonProperty("paymentId") final String paymentId,
+ @JsonProperty("paymentNumber") final String paymentNumber,
+ @JsonProperty("paymentMethodId") final String paymentMethodId,
+ @JsonProperty("requestedDate") final DateTime requestedDate,
+ @JsonProperty("effectiveDate") final DateTime effectiveDate,
+ @JsonProperty("retryCount") final Integer retryCount,
+ @JsonProperty("currency") final String currency,
+ @JsonProperty("status") final String status,
+ @JsonProperty("gatewayErrorCode") final String gatewayErrorCode,
+ @JsonProperty("gatewayErrorMsg") final String gatewayErrorMsg,
+ @JsonProperty("externalBundleKeys") final String bundleKeys,
+ @JsonProperty("refunds") final List<RefundJson> refunds,
+ @JsonProperty("chargebacks") final List<ChargebackJson> chargebacks,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.amount = amount;
+ this.paidAmount = paidAmount;
+ this.invoiceId = invoiceId;
+ this.accountId = accountId;
+ this.paymentId = paymentId;
+ this.paymentNumber = paymentNumber;
+ this.paymentMethodId = paymentMethodId;
+ this.requestedDate = DefaultClock.toUTCDateTime(requestedDate);
+ this.effectiveDate = DefaultClock.toUTCDateTime(effectiveDate);
+ this.currency = currency;
+ this.retryCount = retryCount;
+ this.status = status;
+ this.gatewayErrorCode = gatewayErrorCode;
+ this.gatewayErrorMsg = gatewayErrorMsg;
+ this.bundleKeys = bundleKeys;
+ this.refunds = refunds;
+ this.chargebacks = chargebacks;
+ }
+
+ public PaymentJson(final Payment payment, final String bundleExternalKey,
+ final List<RefundJson> refunds, final List<ChargebackJson> chargebacks) {
+ this(payment, bundleExternalKey, refunds, chargebacks, null);
+ }
+
+ public PaymentJson(final Payment payment, final String bundleExternalKey,
+ final List<RefundJson> refunds, final List<ChargebackJson> chargebacks,
+ @Nullable final List<AuditLog> auditLogs) {
+ this(payment.getAmount(), payment.getPaidAmount(), payment.getAccountId().toString(),
+ payment.getInvoiceId().toString(), payment.getId().toString(),
+ payment.getPaymentNumber().toString(),
+ payment.getPaymentMethodId().toString(),
+ payment.getEffectiveDate(), payment.getEffectiveDate(),
+ payment.getAttempts().size(), payment.getCurrency().toString(), payment.getPaymentStatus().toString(),
+ payment.getAttempts().get(payment.getAttempts().size() - 1).getGatewayErrorCode(),
+ payment.getAttempts().get(payment.getAttempts().size() - 1).getGatewayErrorMsg(),
+ bundleExternalKey, refunds, chargebacks, toAuditLogJson(auditLogs));
+ }
+
+ public PaymentJson(final Payment payment, final List<AuditLog> auditLogs) {
+ this(payment, null, null, null, auditLogs);
+ }
+
+ public String getBundleKeys() {
+ return bundleKeys;
+ }
+
+ public BigDecimal getPaidAmount() {
+ return paidAmount;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public String getInvoiceId() {
+ return invoiceId;
+ }
+
+ public String getPaymentId() {
+ return paymentId;
+ }
+
+ public String getPaymentNumber() {
+ return paymentNumber;
+ }
+
+ public DateTime getRequestedDate() {
+ return requestedDate;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public Integer getRetryCount() {
+ return retryCount;
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getGatewayErrorCode() {
+ return gatewayErrorCode;
+ }
+
+ public String getGatewayErrorMsg() {
+ return gatewayErrorMsg;
+ }
+
+ public String getPaymentMethodId() {
+ return paymentMethodId;
+ }
+
+ public List<RefundJson> getRefunds() {
+ return refunds;
+ }
+
+ public List<ChargebackJson> getChargebacks() {
+ return chargebacks;
+ }
+
+ @Override
+ public String toString() {
+ return "PaymentJson{" +
+ "paidAmount=" + paidAmount +
+ ", amount=" + amount +
+ ", accountId='" + accountId + '\'' +
+ ", invoiceId='" + invoiceId + '\'' +
+ ", paymentId='" + paymentId + '\'' +
+ ", paymentNumber='" + paymentNumber + '\'' +
+ ", requestedDate=" + requestedDate +
+ ", effectiveDate=" + effectiveDate +
+ ", retryCount=" + retryCount +
+ ", currency='" + currency + '\'' +
+ ", status='" + status + '\'' +
+ ", gatewayErrorCode='" + gatewayErrorCode + '\'' +
+ ", gatewayErrorMsg='" + gatewayErrorMsg + '\'' +
+ ", paymentMethodId='" + paymentMethodId + '\'' +
+ ", bundleKeys='" + bundleKeys + '\'' +
+ ", refunds=" + refunds +
+ ", chargebacks=" + chargebacks +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final PaymentJson that = (PaymentJson) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (amount != null ? amount.compareTo(that.amount) != 0 : that.amount != null) {
+ return false;
+ }
+ if (bundleKeys != null ? !bundleKeys.equals(that.bundleKeys) : that.bundleKeys != null) {
+ return false;
+ }
+ if (chargebacks != null ? !chargebacks.equals(that.chargebacks) : that.chargebacks != null) {
+ return false;
+ }
+ if (currency != null ? !currency.equals(that.currency) : that.currency != null) {
+ return false;
+ }
+ if (effectiveDate != null ? effectiveDate.compareTo(that.effectiveDate) != 0 : that.effectiveDate != null) {
+ return false;
+ }
+ if (gatewayErrorCode != null ? !gatewayErrorCode.equals(that.gatewayErrorCode) : that.gatewayErrorCode != null) {
+ return false;
+ }
+ if (gatewayErrorMsg != null ? !gatewayErrorMsg.equals(that.gatewayErrorMsg) : that.gatewayErrorMsg != null) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (paidAmount != null ? paidAmount.compareTo(that.paidAmount) != 0 : that.paidAmount != null) {
+ return false;
+ }
+ if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
+ return false;
+ }
+ if (paymentMethodId != null ? !paymentMethodId.equals(that.paymentMethodId) : that.paymentMethodId != null) {
+ return false;
+ }
+ if (paymentNumber != null ? !paymentNumber.equals(that.paymentNumber) : that.paymentNumber != null) {
+ return false;
+ }
+ if (refunds != null ? !refunds.equals(that.refunds) : that.refunds != null) {
+ return false;
+ }
+ if (requestedDate != null ? requestedDate.compareTo(that.requestedDate) != 0 : that.requestedDate != null) {
+ return false;
+ }
+ if (retryCount != null ? !retryCount.equals(that.retryCount) : that.retryCount != null) {
+ return false;
+ }
+ if (status != null ? !status.equals(that.status) : that.status != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = paidAmount != null ? paidAmount.hashCode() : 0;
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (invoiceId != null ? invoiceId.hashCode() : 0);
+ result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
+ result = 31 * result + (paymentNumber != null ? paymentNumber.hashCode() : 0);
+ result = 31 * result + (requestedDate != null ? requestedDate.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (retryCount != null ? retryCount.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (status != null ? status.hashCode() : 0);
+ result = 31 * result + (gatewayErrorCode != null ? gatewayErrorCode.hashCode() : 0);
+ result = 31 * result + (gatewayErrorMsg != null ? gatewayErrorMsg.hashCode() : 0);
+ result = 31 * result + (paymentMethodId != null ? paymentMethodId.hashCode() : 0);
+ result = 31 * result + (bundleKeys != null ? bundleKeys.hashCode() : 0);
+ result = 31 * result + (refunds != null ? refunds.hashCode() : 0);
+ result = 31 * result + (chargebacks != null ? chargebacks.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentMethodJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentMethodJson.java
new file mode 100644
index 0000000..cd681b6
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentMethodJson.java
@@ -0,0 +1,592 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.payment.api.PaymentMethod;
+import org.killbill.billing.payment.api.PaymentMethodKVInfo;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+
+public class PaymentMethodJson extends JsonBase {
+
+ private final String paymentMethodId;
+ private final String accountId;
+ private final Boolean isDefault;
+ private final String pluginName;
+ private final PaymentMethodPluginDetailJson pluginInfo;
+
+ @JsonCreator
+ public PaymentMethodJson(@JsonProperty("paymentMethodId") final String paymentMethodId,
+ @JsonProperty("accountId") final String accountId,
+ @JsonProperty("isDefault") final Boolean isDefault,
+ @JsonProperty("pluginName") final String pluginName,
+ @JsonProperty("pluginInfo") final PaymentMethodPluginDetailJson pluginInfo,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.paymentMethodId = paymentMethodId;
+ this.accountId = accountId;
+ this.isDefault = isDefault;
+ this.pluginName = pluginName;
+ this.pluginInfo = pluginInfo;
+ }
+
+ public static PaymentMethodJson toPaymentMethodJson(final Account account, final PaymentMethod in, @Nullable final AccountAuditLogs accountAuditLogs) {
+ final boolean isDefault = account.getPaymentMethodId() != null && account.getPaymentMethodId().equals(in.getId());
+ final PaymentMethodPlugin pluginDetail = in.getPluginDetail();
+ PaymentMethodPluginDetailJson pluginDetailJson = null;
+ if (pluginDetail != null) {
+ List<PaymentMethodProperties> properties = null;
+ if (pluginDetail.getProperties() != null) {
+ properties = new ArrayList<PaymentMethodJson.PaymentMethodProperties>(Collections2.transform(pluginDetail.getProperties(), new Function<PaymentMethodKVInfo, PaymentMethodProperties>() {
+ @Override
+ public PaymentMethodProperties apply(final PaymentMethodKVInfo input) {
+ return new PaymentMethodProperties(input.getKey(), input.getValue() == null ? null : input.getValue().toString(), input.getIsUpdatable());
+ }
+ }));
+ }
+ pluginDetailJson = new PaymentMethodPluginDetailJson(pluginDetail.getExternalPaymentMethodId(),
+ pluginDetail.isDefaultPaymentMethod(),
+ pluginDetail.getType(),
+ pluginDetail.getCCName(),
+ pluginDetail.getCCType(),
+ pluginDetail.getCCExpirationMonth(),
+ pluginDetail.getCCExpirationYear(),
+ pluginDetail.getCCLast4(),
+ pluginDetail.getAddress1(),
+ pluginDetail.getAddress2(),
+ pluginDetail.getCity(),
+ pluginDetail.getState(),
+ pluginDetail.getZip(),
+ pluginDetail.getCountry(),
+ properties);
+ }
+ return new PaymentMethodJson(in.getId().toString(), account.getId().toString(), isDefault, in.getPluginName(),
+ pluginDetailJson, toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForPaymentMethod(in.getId())));
+ }
+
+ public PaymentMethod toPaymentMethod(final String accountId) {
+ return new PaymentMethod() {
+ @Override
+ public Boolean isActive() {
+ return true;
+ }
+
+ @Override
+ public String getPluginName() {
+ return pluginName;
+ }
+
+ @Override
+ public UUID getId() {
+ return paymentMethodId != null ? UUID.fromString(paymentMethodId) : null;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return null;
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return null;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return UUID.fromString(accountId);
+ }
+
+ @Override
+ public PaymentMethodPlugin getPluginDetail() {
+ return new PaymentMethodPlugin() {
+ @Override
+ public UUID getKbPaymentMethodId() {
+ return paymentMethodId == null ? null : UUID.fromString(paymentMethodId);
+ }
+
+ @Override
+ public boolean isDefaultPaymentMethod() {
+ // N/A
+ return false;
+ }
+
+ @Override
+ public String getType() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getCCName() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getCCType() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getCCExpirationMonth() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getCCExpirationYear() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getCCLast4() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getAddress1() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getAddress2() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getCity() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getState() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getZip() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getCountry() {
+ // N/A
+ return null;
+ }
+
+ @Override
+ public String getExternalPaymentMethodId() {
+ return pluginInfo.getExternalPaymentId();
+ }
+
+ @Override
+ public List<PaymentMethodKVInfo> getProperties() {
+ if (pluginInfo.getProperties() != null) {
+ final List<PaymentMethodKVInfo> result = new LinkedList<PaymentMethodKVInfo>();
+ for (final PaymentMethodProperties cur : pluginInfo.getProperties()) {
+ result.add(new PaymentMethodKVInfo(cur.getKey(), cur.getValue(), cur.isUpdatable));
+ }
+ return result;
+ }
+ return null;
+ }
+ };
+ }
+ };
+ }
+
+ public String getPaymentMethodId() {
+ return paymentMethodId;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ @JsonProperty("isDefault")
+ public Boolean isDefault() {
+ return isDefault;
+ }
+
+ public String getPluginName() {
+ return pluginName;
+ }
+
+ public PaymentMethodPluginDetailJson getPluginInfo() {
+ return pluginInfo;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("PaymentMethodJson{");
+ sb.append("paymentMethodId='").append(paymentMethodId).append('\'');
+ sb.append(", accountId='").append(accountId).append('\'');
+ sb.append(", isDefault=").append(isDefault);
+ sb.append(", pluginName='").append(pluginName).append('\'');
+ sb.append(", pluginInfo=").append(pluginInfo);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final PaymentMethodJson that = (PaymentMethodJson) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (isDefault != null ? !isDefault.equals(that.isDefault) : that.isDefault != null) {
+ return false;
+ }
+ if (paymentMethodId != null ? !paymentMethodId.equals(that.paymentMethodId) : that.paymentMethodId != null) {
+ return false;
+ }
+ if (pluginInfo != null ? !pluginInfo.equals(that.pluginInfo) : that.pluginInfo != null) {
+ return false;
+ }
+ if (pluginName != null ? !pluginName.equals(that.pluginName) : that.pluginName != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = paymentMethodId != null ? paymentMethodId.hashCode() : 0;
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (isDefault != null ? isDefault.hashCode() : 0);
+ result = 31 * result + (pluginName != null ? pluginName.hashCode() : 0);
+ result = 31 * result + (pluginInfo != null ? pluginInfo.hashCode() : 0);
+ return result;
+ }
+
+ public static class PaymentMethodPluginDetailJson {
+
+ private final String externalPaymentId;
+ private final Boolean isDefaultPaymentMethod;
+ private final String type;
+ private final String ccName;
+ private final String ccType;
+ private final String ccExpirationMonth;
+ private final String ccExpirationYear;
+ private final String ccLast4;
+ private final String address1;
+ private final String address2;
+ private final String city;
+ private final String state;
+ private final String zip;
+ private final String country;
+ private final List<PaymentMethodProperties> properties;
+
+ @JsonCreator
+ public PaymentMethodPluginDetailJson(@JsonProperty("externalPaymentId") final String externalPaymentId,
+ @JsonProperty("isDefaultPaymentMethod") final Boolean isDefaultPaymentMethod,
+ @JsonProperty("type") final String type,
+ @JsonProperty("ccName") final String ccName,
+ @JsonProperty("ccType") final String ccType,
+ @JsonProperty("ccExpirationMonth") final String ccExpirationMonth,
+ @JsonProperty("ccExpirationYear") final String ccExpirationYear,
+ @JsonProperty("ccLast4") final String ccLast4,
+ @JsonProperty("address1") final String address1,
+ @JsonProperty("address2") final String address2,
+ @JsonProperty("city") final String city,
+ @JsonProperty("state") final String state,
+ @JsonProperty("zip") final String zip,
+ @JsonProperty("country") final String country,
+ @JsonProperty("properties") final List<PaymentMethodProperties> properties) {
+ this.externalPaymentId = externalPaymentId;
+ this.isDefaultPaymentMethod = isDefaultPaymentMethod;
+ this.type = type;
+ this.ccName = ccName;
+ this.ccType = ccType;
+ this.ccExpirationMonth = ccExpirationMonth;
+ this.ccExpirationYear = ccExpirationYear;
+ this.ccLast4 = ccLast4;
+ this.address1 = address1;
+ this.address2 = address2;
+ this.city = city;
+ this.state = state;
+ this.zip = zip;
+ this.country = country;
+ this.properties = properties;
+ }
+
+ public String getExternalPaymentId() {
+ return externalPaymentId;
+ }
+
+ public Boolean getIsDefaultPaymentMethod() {
+ return isDefaultPaymentMethod;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getCcName() {
+ return ccName;
+ }
+
+ public String getCcType() {
+ return ccType;
+ }
+
+ public String getCcExpirationMonth() {
+ return ccExpirationMonth;
+ }
+
+ public String getCcExpirationYear() {
+ return ccExpirationYear;
+ }
+
+ public String getCcLast4() {
+ return ccLast4;
+ }
+
+ public String getAddress1() {
+ return address1;
+ }
+
+ public String getAddress2() {
+ return address2;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public String getZip() {
+ return zip;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public List<PaymentMethodProperties> getProperties() {
+ return properties;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("PaymentMethodPluginDetailJson{");
+ sb.append("externalPaymentId='").append(externalPaymentId).append('\'');
+ sb.append(", isDefaultPaymentMethod=").append(isDefaultPaymentMethod);
+ sb.append(", type='").append(type).append('\'');
+ sb.append(", ccName='").append(ccName).append('\'');
+ sb.append(", ccType='").append(ccType).append('\'');
+ sb.append(", ccExpirationMonth='").append(ccExpirationMonth).append('\'');
+ sb.append(", ccExpirationYear='").append(ccExpirationYear).append('\'');
+ sb.append(", ccLast4='").append(ccLast4).append('\'');
+ sb.append(", address1='").append(address1).append('\'');
+ sb.append(", address2='").append(address2).append('\'');
+ sb.append(", city='").append(city).append('\'');
+ sb.append(", state='").append(state).append('\'');
+ sb.append(", zip='").append(zip).append('\'');
+ sb.append(", country='").append(country).append('\'');
+ sb.append(", properties=").append(properties);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final PaymentMethodPluginDetailJson that = (PaymentMethodPluginDetailJson) o;
+
+ if (address1 != null ? !address1.equals(that.address1) : that.address1 != null) {
+ return false;
+ }
+ if (address2 != null ? !address2.equals(that.address2) : that.address2 != null) {
+ return false;
+ }
+ if (ccExpirationMonth != null ? !ccExpirationMonth.equals(that.ccExpirationMonth) : that.ccExpirationMonth != null) {
+ return false;
+ }
+ if (ccExpirationYear != null ? !ccExpirationYear.equals(that.ccExpirationYear) : that.ccExpirationYear != null) {
+ return false;
+ }
+ if (ccLast4 != null ? !ccLast4.equals(that.ccLast4) : that.ccLast4 != null) {
+ return false;
+ }
+ if (ccName != null ? !ccName.equals(that.ccName) : that.ccName != null) {
+ return false;
+ }
+ if (ccType != null ? !ccType.equals(that.ccType) : that.ccType != null) {
+ return false;
+ }
+ if (city != null ? !city.equals(that.city) : that.city != null) {
+ return false;
+ }
+ if (country != null ? !country.equals(that.country) : that.country != null) {
+ return false;
+ }
+ if (externalPaymentId != null ? !externalPaymentId.equals(that.externalPaymentId) : that.externalPaymentId != null) {
+ return false;
+ }
+ if (isDefaultPaymentMethod != null ? !isDefaultPaymentMethod.equals(that.isDefaultPaymentMethod) : that.isDefaultPaymentMethod != null) {
+ return false;
+ }
+ if (properties != null ? !properties.equals(that.properties) : that.properties != null) {
+ return false;
+ }
+ if (state != null ? !state.equals(that.state) : that.state != null) {
+ return false;
+ }
+ if (type != null ? !type.equals(that.type) : that.type != null) {
+ return false;
+ }
+ if (zip != null ? !zip.equals(that.zip) : that.zip != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = externalPaymentId != null ? externalPaymentId.hashCode() : 0;
+ result = 31 * result + (isDefaultPaymentMethod != null ? isDefaultPaymentMethod.hashCode() : 0);
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ result = 31 * result + (ccName != null ? ccName.hashCode() : 0);
+ result = 31 * result + (ccType != null ? ccType.hashCode() : 0);
+ result = 31 * result + (ccExpirationMonth != null ? ccExpirationMonth.hashCode() : 0);
+ result = 31 * result + (ccExpirationYear != null ? ccExpirationYear.hashCode() : 0);
+ result = 31 * result + (ccLast4 != null ? ccLast4.hashCode() : 0);
+ result = 31 * result + (address1 != null ? address1.hashCode() : 0);
+ result = 31 * result + (address2 != null ? address2.hashCode() : 0);
+ result = 31 * result + (city != null ? city.hashCode() : 0);
+ result = 31 * result + (state != null ? state.hashCode() : 0);
+ result = 31 * result + (zip != null ? zip.hashCode() : 0);
+ result = 31 * result + (country != null ? country.hashCode() : 0);
+ result = 31 * result + (properties != null ? properties.hashCode() : 0);
+ return result;
+ }
+ }
+
+ public static final class PaymentMethodProperties {
+
+ private final String key;
+ private final String value;
+ private final Boolean isUpdatable;
+
+ @JsonCreator
+ public PaymentMethodProperties(@JsonProperty("key") final String key,
+ @JsonProperty("value") final String value,
+ @JsonProperty("isUpdatable") final Boolean isUpdatable) {
+ super();
+ this.key = key;
+ this.value = value;
+ this.isUpdatable = isUpdatable;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public Boolean getIsUpdatable() {
+ return isUpdatable;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("PaymentMethodProperties{");
+ sb.append("key='").append(key).append('\'');
+ sb.append(", value='").append(value).append('\'');
+ sb.append(", isUpdatable=").append(isUpdatable);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final PaymentMethodProperties that = (PaymentMethodProperties) o;
+
+ if (isUpdatable != null ? !isUpdatable.equals(that.isUpdatable) : that.isUpdatable != null) {
+ return false;
+ }
+ if (key != null ? !key.equals(that.key) : that.key != null) {
+ return false;
+ }
+ if (value != null ? !value.equals(that.value) : that.value != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = key != null ? key.hashCode() : 0;
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ result = 31 * result + (isUpdatable != null ? isUpdatable.hashCode() : 0);
+ return result;
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PlanDetailJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PlanDetailJson.java
new file mode 100644
index 0000000..1492b13
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PlanDetailJson.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CurrencyValueNull;
+import org.killbill.billing.catalog.api.Listing;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.Price;
+import org.killbill.billing.jaxrs.json.CatalogJsonSimple.PriceJson;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PlanDetailJson {
+
+ final String productName;
+ final String planName;
+ final BillingPeriod billingPeriod;
+ final String priceListName;
+ final List<PriceJson> finalPhasePrice;
+
+ @JsonCreator
+ public PlanDetailJson(@JsonProperty("product") final String productName,
+ @JsonProperty("plan") final String planName,
+ @JsonProperty("final_phase_billing_period") final BillingPeriod billingPeriod,
+ @JsonProperty("priceList") final String priceListName,
+ @JsonProperty("final_phase_recurring_price") final List<PriceJson> finalPhasePrice) {
+ this.productName = productName;
+ this.planName = planName;
+ this.billingPeriod = billingPeriod;
+ this.priceListName = priceListName;
+ this.finalPhasePrice = finalPhasePrice;
+ }
+
+ public PlanDetailJson(final Listing listing) {
+ final Plan plan = listing.getPlan();
+ if (plan == null) {
+ this.productName = null;
+ this.planName = null;
+ this.billingPeriod = null;
+ this.finalPhasePrice = ImmutableList.<PriceJson>of();
+ } else {
+ this.productName = plan.getProduct() == null ? null : plan.getProduct().getName();
+ this.planName = plan.getName();
+ this.billingPeriod = plan.getBillingPeriod();
+ if (plan.getFinalPhase() == null || plan.getFinalPhase().getRecurringPrice() == null || plan.getFinalPhase().getRecurringPrice().getPrices() == null) {
+ this.finalPhasePrice = ImmutableList.<PriceJson>of();
+ } else {
+ this.finalPhasePrice = Lists.transform(ImmutableList.<Price>copyOf(plan.getFinalPhase().getRecurringPrice().getPrices()),
+ new Function<Price, PriceJson>() {
+ @Override
+ public PriceJson apply(final Price price) {
+ try {
+ return new PriceJson(price);
+ } catch (CurrencyValueNull e) {
+ return new PriceJson(price.getCurrency().toString(), BigDecimal.ZERO);
+ }
+ }
+ });
+ }
+ }
+ this.priceListName = listing.getPriceList() == null ? null : listing.getPriceList().getName();
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public String getPlanName() {
+ return planName;
+ }
+
+ public BillingPeriod getBillingPeriod() {
+ return billingPeriod;
+ }
+
+ public String getPriceListName() {
+ return priceListName;
+ }
+
+ public List<PriceJson> getFinalPhasePrice() {
+ return finalPhasePrice;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("PlanDetailJson{");
+ sb.append("productName='").append(productName).append('\'');
+ sb.append(", planName='").append(planName).append('\'');
+ sb.append(", billingPeriod=").append(billingPeriod);
+ sb.append(", priceListName='").append(priceListName).append('\'');
+ sb.append(", finalPhasePrice=").append(finalPhasePrice);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final PlanDetailJson that = (PlanDetailJson) o;
+
+ if (billingPeriod != that.billingPeriod) {
+ return false;
+ }
+ if (finalPhasePrice != null ? !finalPhasePrice.equals(that.finalPhasePrice) : that.finalPhasePrice != null) {
+ return false;
+ }
+ if (planName != null ? !planName.equals(that.planName) : that.planName != null) {
+ return false;
+ }
+ if (priceListName != null ? !priceListName.equals(that.priceListName) : that.priceListName != null) {
+ return false;
+ }
+ if (productName != null ? !productName.equals(that.productName) : that.productName != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = productName != null ? productName.hashCode() : 0;
+ result = 31 * result + (planName != null ? planName.hashCode() : 0);
+ result = 31 * result + (billingPeriod != null ? billingPeriod.hashCode() : 0);
+ result = 31 * result + (priceListName != null ? priceListName.hashCode() : 0);
+ result = 31 * result + (finalPhasePrice != null ? finalPhasePrice.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/RefundJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/RefundJson.java
new file mode 100644
index 0000000..3248f6e
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/RefundJson.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.payment.api.Refund;
+import org.killbill.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+public class RefundJson extends JsonBase {
+
+ private final String refundId;
+ private final String paymentId;
+ private final BigDecimal amount;
+ private final String currency;
+ private final Boolean isAdjusted;
+ private final DateTime requestedDate;
+ private final DateTime effectiveDate;
+ private final String status;
+ private final List<InvoiceItemJson> adjustments;
+
+ @JsonCreator
+ public RefundJson(@JsonProperty("refundId") final String refundId,
+ @JsonProperty("paymentId") final String paymentId,
+ @JsonProperty("amount") final BigDecimal amount,
+ @JsonProperty("currency") final String currency,
+ @JsonProperty("status") final String status,
+ @JsonProperty("adjusted") final Boolean isAdjusted,
+ @JsonProperty("requestedDate") final DateTime requestedDate,
+ @JsonProperty("effectiveDate") final DateTime effectiveDate,
+ @JsonProperty("adjustments") @Nullable final List<InvoiceItemJson> adjustments,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.refundId = refundId;
+ this.paymentId = paymentId;
+ this.amount = amount;
+ this.currency = currency;
+ this.status = status;
+ this.isAdjusted = isAdjusted;
+ this.requestedDate = requestedDate;
+ this.effectiveDate = effectiveDate;
+ this.adjustments = adjustments;
+ }
+
+ public RefundJson(final Refund refund) {
+ this(refund, null, null);
+ }
+
+ public RefundJson(final Refund refund, @Nullable final List<InvoiceItem> adjustments, @Nullable final List<AuditLog> auditLogs) {
+ this(refund.getId().toString(), refund.getPaymentId().toString(), refund.getRefundAmount(), refund.getCurrency().toString(),
+ refund.getRefundStatus().toString(), refund.isAdjusted(), refund.getEffectiveDate(), refund.getEffectiveDate(),
+ adjustments == null ? null : ImmutableList.<InvoiceItemJson>copyOf(Collections2.transform(adjustments, new Function<InvoiceItem, InvoiceItemJson>() {
+ @Override
+ public InvoiceItemJson apply(@Nullable final InvoiceItem input) {
+ return new InvoiceItemJson(input);
+ }
+ })),
+ toAuditLogJson(auditLogs));
+ }
+
+ public String getRefundId() {
+ return refundId;
+ }
+
+ public String getPaymentId() {
+ return paymentId;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public boolean isAdjusted() {
+ return isAdjusted;
+ }
+
+ public DateTime getRequestedDate() {
+ return requestedDate;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public List<InvoiceItemJson> getAdjustments() {
+ return adjustments;
+ }
+
+ public String getStatus() { return status; }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("RefundJson");
+ sb.append("{refundId='").append(refundId).append('\'');
+ sb.append(", paymentId='").append(paymentId).append('\'');
+ sb.append(", amount=").append(amount);
+ sb.append(", currency=").append(currency);
+ sb.append(", status=").append(status);
+ sb.append(", isAdjusted=").append(isAdjusted);
+ sb.append(", requestedDate=").append(requestedDate);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", adjustments=").append(adjustments);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ int result = refundId != null ? refundId.hashCode() : 0;
+ result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (status != null ? status.hashCode() : 0);
+ result = 31 * result + (isAdjusted != null ? isAdjusted.hashCode() : 0);
+ result = 31 * result + (requestedDate != null ? requestedDate.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (adjustments != null ? adjustments.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!this.equalsNoIdNoDates(obj)) {
+ return false;
+ } else {
+ final RefundJson other = (RefundJson) obj;
+ if (refundId == null) {
+ if (other.getRefundId() != null) {
+ return false;
+ }
+ } else if (!refundId.equals(other.getRefundId())) {
+ return false;
+ }
+
+ if (requestedDate == null) {
+ if (other.getRequestedDate() != null) {
+ return false;
+ }
+ } else if (requestedDate.compareTo(other.getRequestedDate()) != 0) {
+ return false;
+ }
+
+ if (effectiveDate == null) {
+ if (other.getEffectiveDate() != null) {
+ return false;
+ }
+ } else if (effectiveDate.compareTo(other.getEffectiveDate()) != 0) {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ public boolean equalsNoIdNoDates(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (obj == null) {
+ return false;
+ }
+
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final RefundJson other = (RefundJson) obj;
+ if (isAdjusted == null) {
+ if (other.isAdjusted != null) {
+ return false;
+ }
+ } else if (!isAdjusted.equals(other.isAdjusted)) {
+ return false;
+ }
+
+ if (paymentId == null) {
+ if (other.paymentId != null) {
+ return false;
+ }
+ } else if (!paymentId.equals(other.paymentId)) {
+ return false;
+ }
+
+ if (amount == null) {
+ if (other.amount != null) {
+ return false;
+ }
+ } else if (!amount.equals(other.amount)) {
+ return false;
+ }
+
+ if (currency == null) {
+ if (other.currency != null) {
+ return false;
+ }
+ } else if (!currency.equals(other.currency)) {
+ return false;
+ }
+
+ if (status == null) {
+ if (other.status != null) {
+ return false;
+ }
+ } else if (!status.equals(other.status)) {
+ return false;
+ }
+
+ if (adjustments == null) {
+ if (other.adjustments != null) {
+ return false;
+ }
+ } else if (!adjustments.equals(other.adjustments)) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SessionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SessionJson.java
new file mode 100644
index 0000000..fbd5eca
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SessionJson.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import org.apache.shiro.session.Session;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class SessionJson {
+
+ private final String id;
+ private final DateTime startDate;
+ private final DateTime lastAccessDate;
+ private final Long timeout;
+ private final String host;
+
+ @JsonCreator
+ public SessionJson(@JsonProperty("id") final String id,
+ @JsonProperty("startDate") final DateTime startDate,
+ @JsonProperty("lastAccessDate") final DateTime lastAccessDate,
+ @JsonProperty("timeout") final Long timeout,
+ @JsonProperty("host") final String host) {
+ this.id = id;
+ this.startDate = startDate;
+ this.lastAccessDate = lastAccessDate;
+ this.timeout = timeout;
+ this.host = host;
+ }
+
+ public SessionJson(final Session session) {
+ this.id = session.getId() == null ? null : session.getId().toString();
+ this.startDate = session.getStartTimestamp() == null ? null : new DateTime(session.getStartTimestamp(), DateTimeZone.UTC);
+ this.lastAccessDate = session.getLastAccessTime() == null ? null : new DateTime(session.getLastAccessTime(), DateTimeZone.UTC);
+ this.timeout = session.getTimeout();
+ this.host = session.getHost();
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public DateTime getStartDate() {
+ return startDate;
+ }
+
+ public DateTime getLastAccessDate() {
+ return lastAccessDate;
+ }
+
+ public Long getTimeout() {
+ return timeout;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("SessionJson{");
+ sb.append("id='").append(id).append('\'');
+ sb.append(", startDate=").append(startDate);
+ sb.append(", lastAccessDate=").append(lastAccessDate);
+ sb.append(", timeout=").append(timeout);
+ sb.append(", host='").append(host).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final SessionJson that = (SessionJson) o;
+
+ if (host != null ? !host.equals(that.host) : that.host != null) {
+ return false;
+ }
+ if (id != null ? !id.equals(that.id) : that.id != null) {
+ return false;
+ }
+ if (lastAccessDate != null ? !lastAccessDate.equals(that.lastAccessDate) : that.lastAccessDate != null) {
+ return false;
+ }
+ if (startDate != null ? !startDate.equals(that.startDate) : that.startDate != null) {
+ return false;
+ }
+ if (timeout != null ? !timeout.equals(that.timeout) : that.timeout != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + (startDate != null ? startDate.hashCode() : 0);
+ result = 31 * result + (lastAccessDate != null ? lastAccessDate.hashCode() : 0);
+ result = 31 * result + (timeout != null ? timeout.hashCode() : 0);
+ result = 31 * result + (host != null ? host.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubjectJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubjectJson.java
new file mode 100644
index 0000000..7f95d87
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubjectJson.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import javax.annotation.Nullable;
+
+import org.apache.shiro.session.Session;
+import org.apache.shiro.subject.Subject;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class SubjectJson {
+
+ private final String principal;
+ private final Boolean isAuthenticated;
+ private final Boolean isRemembered;
+ private final SessionJson session;
+
+ @JsonCreator
+ public SubjectJson(@JsonProperty("principal") final String principal,
+ @JsonProperty("isAuthenticated") final Boolean isAuthenticated,
+ @JsonProperty("isRemembered") final Boolean isRemembered,
+ @JsonProperty("session") @Nullable final SessionJson session) {
+ this.principal = principal;
+ this.isAuthenticated = isAuthenticated;
+ this.isRemembered = isRemembered;
+ this.session = session;
+ }
+
+ public SubjectJson(final Subject subject) {
+ this.principal = subject.getPrincipal() == null ? null : subject.getPrincipal().toString();
+ this.isAuthenticated = subject.isAuthenticated();
+ this.isRemembered = subject.isRemembered();
+ final Session subjectSession = subject.getSession(false);
+ this.session = subjectSession == null ? null : new SessionJson(subjectSession);
+ }
+
+ public String getPrincipal() {
+ return principal;
+ }
+
+ public Boolean getIsAuthenticated() {
+ return isAuthenticated;
+ }
+
+ public Boolean getIsRemembered() {
+ return isRemembered;
+ }
+
+ public SessionJson getSession() {
+ return session;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("SubjectJson{");
+ sb.append("principal='").append(principal).append('\'');
+ sb.append(", isAuthenticated=").append(isAuthenticated);
+ sb.append(", isRemembered=").append(isRemembered);
+ sb.append(", session=").append(session);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final SubjectJson that = (SubjectJson) o;
+
+ if (isAuthenticated != null ? !isAuthenticated.equals(that.isAuthenticated) : that.isAuthenticated != null) {
+ return false;
+ }
+ if (isRemembered != null ? !isRemembered.equals(that.isRemembered) : that.isRemembered != null) {
+ return false;
+ }
+ if (principal != null ? !principal.equals(that.principal) : that.principal != null) {
+ return false;
+ }
+ if (session != null ? !session.equals(that.session) : that.session != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = principal != null ? principal.hashCode() : 0;
+ result = 31 * result + (isAuthenticated != null ? isAuthenticated.hashCode() : 0);
+ result = 31 * result + (isRemembered != null ? isRemembered.hashCode() : 0);
+ result = 31 * result + (session != null ? session.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
new file mode 100644
index 0000000..6959a1a
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.entitlement.api.Subscription;
+import org.killbill.billing.entitlement.api.SubscriptionEvent;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class SubscriptionJson extends JsonBase {
+
+ private final String accountId;
+ private final String bundleId;
+ private final String subscriptionId;
+ private final String externalKey;
+ private final LocalDate startDate;
+ private final String productName;
+ private final String productCategory;
+ private final String billingPeriod;
+ private final String priceList;
+ private final LocalDate cancelledDate;
+ private final LocalDate chargedThroughDate;
+ private final LocalDate billingStartDate;
+ private final LocalDate billingEndDate;
+ private final List<EventSubscriptionJson> events;
+ private final List<DeletedEventSubscriptionJson> deletedEvents;
+ private final List<NewEventSubscriptionJson> newEvents;
+
+ public static class EventSubscriptionJson extends EventBaseSubscriptionJson {
+
+ private final String eventId;
+ private final LocalDate effectiveDate;
+
+ @JsonCreator
+ public EventSubscriptionJson(@JsonProperty("eventId") final String eventId,
+ @JsonProperty("billingPeriod") final String billingPeriod,
+ @JsonProperty("requestedDt") final LocalDate requestedDate,
+ @JsonProperty("effectiveDt") final LocalDate effectiveDate,
+ @JsonProperty("product") final String product,
+ @JsonProperty("priceList") final String priceList,
+ @JsonProperty("eventType") final String eventType,
+ @JsonProperty("phase") final String phase,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(billingPeriod, requestedDate, product, priceList, eventType, phase, auditLogs);
+ this.eventId = eventId;
+ this.effectiveDate = effectiveDate;
+ }
+
+ public String getEventId() {
+ return eventId;
+ }
+
+ public LocalDate getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public String toString() {
+ return "EventSubscriptionJson [eventId=" + eventId
+ + ", effectiveDate=" + effectiveDate
+ + ", getBillingPeriod()=" + getBillingPeriod()
+ + ", getRequestedDate()=" + getRequestedDate()
+ + ", getProduct()=" + getProduct() + ", getPriceList()="
+ + getPriceList() + ", getEventType()=" + getEventType()
+ + ", getPhase()=" + getPhase() + ", getClass()="
+ + getClass() + ", hashCode()=" + hashCode()
+ + ", toString()=" + super.toString() + "]";
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final EventSubscriptionJson that = (EventSubscriptionJson) o;
+
+ if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+ return false;
+ }
+ if (eventId != null ? !eventId.equals(that.eventId) : that.eventId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = eventId != null ? eventId.hashCode() : 0;
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ return result;
+ }
+ }
+
+ public static class DeletedEventSubscriptionJson extends EventSubscriptionJson {
+
+ @JsonCreator
+ public DeletedEventSubscriptionJson(@JsonProperty("eventId") final String eventId,
+ @JsonProperty("billingPeriod") final String billingPeriod,
+ @JsonProperty("requestedDate") final LocalDate requestedDate,
+ @JsonProperty("effectiveDate") final LocalDate effectiveDate,
+ @JsonProperty("product") final String product,
+ @JsonProperty("priceList") final String priceList,
+ @JsonProperty("eventType") final String eventType,
+ @JsonProperty("phase") final String phase,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(eventId, billingPeriod, requestedDate, effectiveDate, product, priceList, eventType, phase, auditLogs);
+ }
+ }
+
+ public static class NewEventSubscriptionJson extends EventBaseSubscriptionJson {
+
+ @JsonCreator
+ public NewEventSubscriptionJson(@JsonProperty("billingPeriod") final String billingPeriod,
+ @JsonProperty("requestedDate") final LocalDate requestedDate,
+ @JsonProperty("product") final String product,
+ @JsonProperty("priceList") final String priceList,
+ @JsonProperty("eventType") final String eventType,
+ @JsonProperty("phase") final String phase,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(billingPeriod, requestedDate, product, priceList, eventType, phase, auditLogs);
+ }
+
+ @Override
+ public String toString() {
+ return "NewEventSubscriptionJson [getBillingPeriod()="
+ + getBillingPeriod() + ", getRequestedDate()="
+ + getRequestedDate() + ", getProduct()=" + getProduct()
+ + ", getPriceList()=" + getPriceList()
+ + ", getEventType()=" + getEventType() + ", getPhase()="
+ + getPhase() + ", getClass()=" + getClass()
+ + ", hashCode()=" + hashCode() + ", toString()="
+ + super.toString() + "]";
+ }
+ }
+
+ public abstract static class EventBaseSubscriptionJson extends JsonBase {
+
+ private final String billingPeriod;
+ private final LocalDate requestedDate;
+ private final String product;
+ private final String priceList;
+ private final String eventType;
+ private final String phase;
+
+ @JsonCreator
+ public EventBaseSubscriptionJson(@JsonProperty("billingPeriod") final String billingPeriod,
+ @JsonProperty("requestedDate") final LocalDate requestedDate,
+ @JsonProperty("product") final String product,
+ @JsonProperty("priceList") final String priceList,
+ @JsonProperty("eventType") final String eventType,
+ @JsonProperty("phase") final String phase,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.billingPeriod = billingPeriod;
+ this.requestedDate = requestedDate;
+ this.product = product;
+ this.priceList = priceList;
+ this.eventType = eventType;
+ this.phase = phase;
+ }
+
+ public String getBillingPeriod() {
+ return billingPeriod;
+ }
+
+ public LocalDate getRequestedDate() {
+ return requestedDate;
+ }
+
+ public String getProduct() {
+ return product;
+ }
+
+ public String getPriceList() {
+ return priceList;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ public String getPhase() {
+ return phase;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("EventBaseSubscriptionJson");
+ sb.append("{billingPeriod='").append(billingPeriod).append('\'');
+ sb.append(", requestedDate=").append(requestedDate);
+ sb.append(", product='").append(product).append('\'');
+ sb.append(", priceList='").append(priceList).append('\'');
+ sb.append(", eventType='").append(eventType).append('\'');
+ sb.append(", phase='").append(phase).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final EventBaseSubscriptionJson that = (EventBaseSubscriptionJson) o;
+
+ if (billingPeriod != null ? !billingPeriod.equals(that.billingPeriod) : that.billingPeriod != null) {
+ return false;
+ }
+ if (eventType != null ? !eventType.equals(that.eventType) : that.eventType != null) {
+ return false;
+ }
+ if (phase != null ? !phase.equals(that.phase) : that.phase != null) {
+ return false;
+ }
+ if (priceList != null ? !priceList.equals(that.priceList) : that.priceList != null) {
+ return false;
+ }
+ if (product != null ? !product.equals(that.product) : that.product != null) {
+ return false;
+ }
+ if (requestedDate != null ? !requestedDate.equals(that.requestedDate) : that.requestedDate != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = billingPeriod != null ? billingPeriod.hashCode() : 0;
+ result = 31 * result + (requestedDate != null ? requestedDate.hashCode() : 0);
+ result = 31 * result + (product != null ? product.hashCode() : 0);
+ result = 31 * result + (priceList != null ? priceList.hashCode() : 0);
+ result = 31 * result + (eventType != null ? eventType.hashCode() : 0);
+ result = 31 * result + (phase != null ? phase.hashCode() : 0);
+ return result;
+ }
+ }
+
+ @JsonCreator
+ public SubscriptionJson(@JsonProperty("accountId") @Nullable final String accountId,
+ @JsonProperty("bundleId") @Nullable final String bundleId,
+ @JsonProperty("subscriptionId") @Nullable final String subscriptionId,
+ @JsonProperty("externalKey") @Nullable final String externalKey,
+ @JsonProperty("startDate") @Nullable final LocalDate startDate,
+ @JsonProperty("productName") @Nullable final String productName,
+ @JsonProperty("productCategory") @Nullable final String productCategory,
+ @JsonProperty("billingPeriod") @Nullable final String billingPeriod,
+ @JsonProperty("priceList") @Nullable final String priceList,
+ @JsonProperty("cancelledDate") @Nullable final LocalDate cancelledDate,
+ @JsonProperty("chargedThroughDate") @Nullable final LocalDate chargedThroughDate,
+ @JsonProperty("billingStartDate") @Nullable final LocalDate billingStartDate,
+ @JsonProperty("billingEndDate") @Nullable final LocalDate billingEndDate,
+ @JsonProperty("events") @Nullable final List<EventSubscriptionJson> events,
+ @JsonProperty("newEvents") @Nullable final List<NewEventSubscriptionJson> newEvents,
+ @JsonProperty("deletedEvents") @Nullable final List<DeletedEventSubscriptionJson> deletedEvents,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.startDate = startDate;
+ this.productName = productName;
+ this.productCategory = productCategory;
+ this.billingPeriod = billingPeriod;
+ this.priceList = priceList;
+ this.cancelledDate = cancelledDate;
+ this.chargedThroughDate = chargedThroughDate;
+ this.billingStartDate = billingStartDate;
+ this.billingEndDate = billingEndDate;
+ this.accountId = accountId;
+ this.bundleId = bundleId;
+ this.subscriptionId = subscriptionId;
+ this.externalKey = externalKey;
+ this.events = events;
+ this.deletedEvents = deletedEvents;
+ this.newEvents = newEvents;
+ }
+
+ public SubscriptionJson(final Subscription subscription,
+ final List<SubscriptionEvent> subscriptionEvents,
+ @Nullable final AccountAuditLogs accountAuditLogs) {
+ super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForSubscription(subscription.getId())));
+ this.startDate = subscription.getEffectiveStartDate();
+ this.productName = subscription.getLastActiveProduct().getName();
+ this.productCategory = subscription.getLastActiveProductCategory().name();
+ this.billingPeriod = subscription.getLastActivePlan().getBillingPeriod().toString();
+ this.priceList = subscription.getLastActivePriceList().getName();
+ this.cancelledDate = subscription.getEffectiveEndDate();
+ this.chargedThroughDate = subscription.getChargedThroughDate();
+ this.billingStartDate = subscription.getBillingStartDate();
+ this.billingEndDate = subscription.getBillingEndDate();
+ this.accountId = subscription.getAccountId().toString();
+ this.bundleId = subscription.getBundleId().toString();
+ this.subscriptionId = subscription.getId().toString();
+ this.externalKey = subscription.getExternalKey();
+ this.events = subscriptionEvents != null ? new LinkedList<EventSubscriptionJson>() : null;
+ if (events != null) {
+ for (final SubscriptionEvent cur : subscriptionEvents) {
+ final BillingPeriod billingPeriod = cur.getNextBillingPeriod() != null ? cur.getNextBillingPeriod() : cur.getPrevBillingPeriod();
+ final Product product = cur.getNextProduct() != null ? cur.getNextProduct() : cur.getPrevProduct();
+ final PriceList priceList = cur.getNextPriceList() != null ? cur.getNextPriceList() : cur.getPrevPriceList();
+ final PlanPhase phase = cur.getNextPhase() != null ? cur.getNextPhase() : cur.getPrevPhase();
+ this.events.add(new EventSubscriptionJson(cur.getId().toString(),
+ billingPeriod != null ? billingPeriod.toString() : null,
+ cur.getRequestedDate(),
+ cur.getEffectiveDate(),
+ product != null ? product.getName() : null,
+ priceList != null ? priceList.getName() : null,
+ cur.getSubscriptionEventType().toString(),
+ phase != null ? phase.getName() : null,
+ toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForSubscriptionEvent(cur.getId()))));
+ }
+ }
+ this.newEvents = null;
+ this.deletedEvents = null;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public String getBundleId() {
+ return bundleId;
+ }
+
+ public String getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public String getProductCategory() {
+ return productCategory;
+ }
+
+ public String getBillingPeriod() {
+ return billingPeriod;
+ }
+
+ public String getPriceList() {
+ return priceList;
+ }
+
+ public LocalDate getCancelledDate() {
+ return cancelledDate;
+ }
+
+ public LocalDate getChargedThroughDate() {
+ return chargedThroughDate;
+ }
+
+ public LocalDate getBillingStartDate() {
+ return billingStartDate;
+ }
+
+ public LocalDate getBillingEndDate() {
+ return billingEndDate;
+ }
+
+ public List<EventSubscriptionJson> getEvents() {
+ return events;
+ }
+
+ public List<DeletedEventSubscriptionJson> getDeletedEvents() {
+ return deletedEvents;
+ }
+
+ public List<NewEventSubscriptionJson> getNewEvents() {
+ return newEvents;
+ }
+
+ @Override
+ public String toString() {
+ return "SubscriptionJson{" +
+ "accountId='" + accountId + '\'' +
+ ", bundleId='" + bundleId + '\'' +
+ ", subscriptionId='" + subscriptionId + '\'' +
+ ", externalKey='" + externalKey + '\'' +
+ ", startDate=" + startDate +
+ ", productName='" + productName + '\'' +
+ ", productCategory='" + productCategory + '\'' +
+ ", billingPeriod='" + billingPeriod + '\'' +
+ ", priceList='" + priceList + '\'' +
+ ", cancelledDate=" + cancelledDate +
+ ", chargedThroughDate=" + chargedThroughDate +
+ ", billingStartDate=" + billingStartDate +
+ ", billingEndDate=" + billingEndDate +
+ ", events=" + events +
+ ", deletedEvents=" + deletedEvents +
+ ", newEvents=" + newEvents +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final SubscriptionJson that = (SubscriptionJson) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (billingEndDate != null ? billingEndDate.compareTo(that.billingEndDate) != 0 : that.billingEndDate != null) {
+ return false;
+ }
+ if (billingPeriod != null ? !billingPeriod.equals(that.billingPeriod) : that.billingPeriod != null) {
+ return false;
+ }
+ if (billingStartDate != null ? billingStartDate.compareTo(that.billingStartDate) != 0 : that.billingStartDate != null) {
+ return false;
+ }
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (cancelledDate != null ? cancelledDate.compareTo(that.cancelledDate) != 0 : that.cancelledDate != null) {
+ return false;
+ }
+ if (chargedThroughDate != null ? chargedThroughDate.compareTo(that.chargedThroughDate) != 0 : that.chargedThroughDate != null) {
+ return false;
+ }
+ if (deletedEvents != null ? !deletedEvents.equals(that.deletedEvents) : that.deletedEvents != null) {
+ return false;
+ }
+ if (events != null ? !events.equals(that.events) : that.events != null) {
+ return false;
+ }
+ if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
+ return false;
+ }
+ if (newEvents != null ? !newEvents.equals(that.newEvents) : that.newEvents != null) {
+ return false;
+ }
+ if (priceList != null ? !priceList.equals(that.priceList) : that.priceList != null) {
+ return false;
+ }
+ if (productCategory != null ? !productCategory.equals(that.productCategory) : that.productCategory != null) {
+ return false;
+ }
+ if (productName != null ? !productName.equals(that.productName) : that.productName != null) {
+ return false;
+ }
+ if (startDate != null ? startDate.compareTo(that.startDate) != 0 : that.startDate != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = accountId != null ? accountId.hashCode() : 0;
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (subscriptionId != null ? subscriptionId.hashCode() : 0);
+ result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+ result = 31 * result + (startDate != null ? startDate.hashCode() : 0);
+ result = 31 * result + (productName != null ? productName.hashCode() : 0);
+ result = 31 * result + (productCategory != null ? productCategory.hashCode() : 0);
+ result = 31 * result + (billingPeriod != null ? billingPeriod.hashCode() : 0);
+ result = 31 * result + (priceList != null ? priceList.hashCode() : 0);
+ result = 31 * result + (cancelledDate != null ? cancelledDate.hashCode() : 0);
+ result = 31 * result + (chargedThroughDate != null ? chargedThroughDate.hashCode() : 0);
+ result = 31 * result + (billingStartDate != null ? billingStartDate.hashCode() : 0);
+ result = 31 * result + (billingEndDate != null ? billingEndDate.hashCode() : 0);
+ result = 31 * result + (events != null ? events.hashCode() : 0);
+ result = 31 * result + (deletedEvents != null ? deletedEvents.hashCode() : 0);
+ result = 31 * result + (newEvents != null ? newEvents.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TagDefinitionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TagDefinitionJson.java
new file mode 100644
index 0000000..fd1c5cb
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TagDefinitionJson.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+public class TagDefinitionJson extends JsonBase {
+
+ private final String id;
+ private final Boolean isControlTag;
+ private final String name;
+ private final String description;
+ private final List<String> applicableObjectTypes;
+
+ @JsonCreator
+ public TagDefinitionJson(@JsonProperty("id") final String id,
+ @JsonProperty("isControlTag") final Boolean isControlTag,
+ @JsonProperty("name") final String name,
+ @JsonProperty("description") @Nullable final String description,
+ @JsonProperty("applicableObjectTypes") @Nullable final List<String> applicableObjectTypes,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.id = id;
+ this.isControlTag = isControlTag;
+ this.name = name;
+ this.description = description;
+ this.applicableObjectTypes = applicableObjectTypes;
+ }
+
+ public TagDefinitionJson(final TagDefinition tagDefinition, @Nullable final List<AuditLog> auditLogs) {
+ this(tagDefinition.getId().toString(),
+ tagDefinition.isControlTag(),
+ tagDefinition.getName(),
+ tagDefinition.getDescription(),
+ ImmutableList.<String>copyOf(Collections2.transform(tagDefinition.getApplicableObjectTypes(), new Function<ObjectType, String>() {
+ @Override
+ public String apply(@Nullable final ObjectType input) {
+ if (input == null) {
+ return "";
+ } else {
+ return input.toString();
+ }
+ }
+ })),
+ toAuditLogJson(auditLogs));
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ @JsonProperty("isControlTag")
+ public Boolean isControlTag() {
+ return isControlTag;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public List<String> getApplicableObjectTypes() {
+ return applicableObjectTypes;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TagDefinitionJson");
+ sb.append("{id='").append(id).append('\'');
+ sb.append(", isControlTag=").append(isControlTag);
+ sb.append(", name='").append(name).append('\'');
+ sb.append(", description='").append(description).append('\'');
+ sb.append(", applicableObjectTypes='").append(applicableObjectTypes).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final TagDefinitionJson that = (TagDefinitionJson) o;
+
+ if (!equalsNoId(that)) {
+ return false;
+ }
+ if (id != null ? !id.equals(that.id) : that.id != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean equalsNoId(final TagDefinitionJson that) {
+ if (description != null ? !description.equals(that.description) : that.description != null) {
+ return false;
+ }
+ if (isControlTag != null ? !isControlTag.equals(that.isControlTag) : that.isControlTag != null) {
+ return false;
+ }
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+ if (applicableObjectTypes != null ? !applicableObjectTypes.equals(that.applicableObjectTypes) : that.applicableObjectTypes != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + (isControlTag != null ? isControlTag.hashCode() : 0);
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (applicableObjectTypes != null ? applicableObjectTypes.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TagJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TagJson.java
new file mode 100644
index 0000000..6307410
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TagJson.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.tag.Tag;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class TagJson extends JsonBase {
+
+ private final String tagId;
+ private final ObjectType objectType;
+ private final String tagDefinitionId;
+ private final String tagDefinitionName;
+
+ @JsonCreator
+ public TagJson(@JsonProperty("tagId") final String tagId,
+ @JsonProperty("objectType") final ObjectType objectType,
+ @JsonProperty("tagDefinitionId") final String tagDefinitionId,
+ @JsonProperty("tagDefinitionName") final String tagDefinitionName,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.tagId = tagId;
+ this.objectType = objectType;
+ this.tagDefinitionId = tagDefinitionId;
+ this.tagDefinitionName = tagDefinitionName;
+ }
+
+ public TagJson(final Tag tag, final TagDefinition tagDefinition, @Nullable final List<AuditLog> auditLogs) {
+ this(tag.getId().toString(), tag.getObjectType(), tagDefinition.getId().toString(), tagDefinition.getName(), toAuditLogJson(auditLogs));
+ }
+
+ public String getTagId() {
+ return tagId;
+ }
+
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ public String getTagDefinitionId() {
+ return tagDefinitionId;
+ }
+
+ public String getTagDefinitionName() {
+ return tagDefinitionName;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("TagJson{");
+ sb.append("tagId='").append(tagId).append('\'');
+ sb.append(", objectType=").append(objectType);
+ sb.append(", tagDefinitionId='").append(tagDefinitionId).append('\'');
+ sb.append(", tagDefinitionName='").append(tagDefinitionName).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final TagJson tagJson = (TagJson) o;
+
+ if (objectType != tagJson.objectType) {
+ return false;
+ }
+ if (tagDefinitionId != null ? !tagDefinitionId.equals(tagJson.tagDefinitionId) : tagJson.tagDefinitionId != null) {
+ return false;
+ }
+ if (tagDefinitionName != null ? !tagDefinitionName.equals(tagJson.tagDefinitionName) : tagJson.tagDefinitionName != null) {
+ return false;
+ }
+ if (tagId != null ? !tagId.equals(tagJson.tagId) : tagJson.tagId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tagId != null ? tagId.hashCode() : 0;
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ result = 31 * result + (tagDefinitionId != null ? tagDefinitionId.hashCode() : 0);
+ result = 31 * result + (tagDefinitionName != null ? tagDefinitionName.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TenantJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TenantJson.java
new file mode 100644
index 0000000..6670881
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TenantJson.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.tenant.api.TenantData;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class TenantJson extends JsonBase {
+
+ protected final String tenantId;
+ protected final String externalKey;
+ protected final String apiKey;
+ protected final String apiSecret;
+
+ @JsonCreator
+ public TenantJson(@JsonProperty("tenantId") final String tenantId,
+ @JsonProperty("externalKey") final String externalKey,
+ @JsonProperty("apiKey") final String apiKey,
+ @JsonProperty("apiSecret") final String apiSecret) {
+ this.tenantId = tenantId;
+ this.externalKey = externalKey;
+ this.apiKey = apiKey;
+ this.apiSecret = apiSecret;
+ }
+
+ public TenantJson(final Tenant tenant) {
+ this(tenant.getId().toString(), tenant.getExternalKey(), tenant.getApiKey(), tenant.getApiSecret());
+ }
+
+ public TenantData toTenantData() {
+ return new TenantData() {
+ @Override
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ @Override
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ @Override
+ public String getApiSecret() {
+ return apiSecret;
+ }
+ };
+ }
+
+ public String getTenantId() {
+ return tenantId;
+ }
+
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ public String getApiSecret() {
+ return apiSecret;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TenantJson");
+ sb.append("{tenantId='").append(tenantId).append('\'');
+ sb.append(", externalKey='").append(externalKey).append('\'');
+ sb.append(", apiKey='").append(apiKey).append('\'');
+ sb.append(", apiSecret='").append(apiSecret).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final TenantJson that = (TenantJson) o;
+
+ if (apiKey != null ? !apiKey.equals(that.apiKey) : that.apiKey != null) {
+ return false;
+ }
+ if (apiSecret != null ? !apiSecret.equals(that.apiSecret) : that.apiSecret != null) {
+ return false;
+ }
+ if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
+ return false;
+ }
+ if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tenantId != null ? tenantId.hashCode() : 0;
+ result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+ result = 31 * result + (apiKey != null ? apiKey.hashCode() : 0);
+ result = 31 * result + (apiSecret != null ? apiSecret.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TenantKeyJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TenantKeyJson.java
new file mode 100644
index 0000000..51d4bdf
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/TenantKeyJson.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class TenantKeyJson {
+
+ private final String key;
+ private final List<String> values;
+
+
+ @JsonCreator
+ public TenantKeyJson(@JsonProperty("key") final String key,
+ @JsonProperty("values") final List<String> values) {
+ this.key = key;
+ this.values = values;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public List<String> getValues() {
+ return values;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/AccountApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/AccountApiExceptionMapper.java
new file mode 100644
index 0000000..f921220
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/AccountApiExceptionMapper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.AccountApiException;
+
+@Singleton
+@Provider
+public class AccountApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<AccountApiException> {
+
+ private final UriInfo uriInfo;
+
+ public AccountApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final AccountApiException exception) {
+ if (exception.getCode() == ErrorCode.ACCOUNT_ALREADY_EXISTS.getCode()) {
+ return buildConflictingRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.ACCOUNT_CANNOT_CHANGE_EXTERNAL_KEY.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.ACCOUNT_CANNOT_MAP_NULL_KEY.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.ACCOUNT_CREATION_FAILED.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_KEY.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.ACCOUNT_INVALID_NAME.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.ACCOUNT_UPDATE_FAILED.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_RECORD_ID.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else {
+ return fallback(exception, uriInfo);
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/BillingExceptionBaseMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/BillingExceptionBaseMapper.java
new file mode 100644
index 0000000..d2a6734
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/BillingExceptionBaseMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.BillingExceptionBase;
+
+@Singleton
+@Provider
+public class BillingExceptionBaseMapper extends ExceptionMapperBase implements ExceptionMapper<BillingExceptionBase> {
+
+ private final UriInfo uriInfo;
+
+ public BillingExceptionBaseMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final BillingExceptionBase exception) {
+ return buildBadRequestResponse(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/BlockingApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/BlockingApiExceptionMapper.java
new file mode 100644
index 0000000..15d8b78
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/BlockingApiExceptionMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.entitlement.api.BlockingApiException;
+
+@Singleton
+@Provider
+public class BlockingApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<BlockingApiException> {
+
+ private final UriInfo uriInfo;
+
+ public BlockingApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final BlockingApiException exception) {
+ return fallback(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/CatalogApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/CatalogApiExceptionMapper.java
new file mode 100644
index 0000000..d49d752
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/CatalogApiExceptionMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.catalog.api.CatalogApiException;
+
+@Singleton
+@Provider
+public class CatalogApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<CatalogApiException> {
+
+ private final UriInfo uriInfo;
+
+ public CatalogApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final CatalogApiException exception) {
+ return fallback(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/CurrencyValueNullMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/CurrencyValueNullMapper.java
new file mode 100644
index 0000000..ba3cc75
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/CurrencyValueNullMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.catalog.api.CurrencyValueNull;
+
+@Singleton
+@Provider
+public class CurrencyValueNullMapper extends ExceptionMapperBase implements ExceptionMapper<CurrencyValueNull> {
+
+ private final UriInfo uriInfo;
+
+ public CurrencyValueNullMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final CurrencyValueNull exception) {
+ return buildBadRequestResponse(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EmailApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EmailApiExceptionMapper.java
new file mode 100644
index 0000000..c6102c6
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EmailApiExceptionMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.util.email.EmailApiException;
+
+@Singleton
+@Provider
+public class EmailApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<EmailApiException> {
+
+ private final UriInfo uriInfo;
+
+ public EmailApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final EmailApiException exception) {
+ return fallback(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EntitlementApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EntitlementApiExceptionMapper.java
new file mode 100644
index 0000000..e31c50e
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EntitlementApiExceptionMapper.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.entitlement.api.EntitlementApiException;
+
+@Singleton
+@Provider
+public class EntitlementApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<EntitlementApiException> {
+
+ private final UriInfo uriInfo;
+
+ public EntitlementApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final EntitlementApiException exception) {
+ if (exception.getCode() == ErrorCode.SUB_CANCEL_BAD_STATE.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CHANGE_NON_ACTIVE.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_INVALID_SUBSCRIPTION_ID.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else {
+ return fallback(exception, uriInfo);
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EntityPersistenceExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EntityPersistenceExceptionMapper.java
new file mode 100644
index 0000000..f33df9d
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/EntityPersistenceExceptionMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.entity.EntityPersistenceException;
+
+@Singleton
+@Provider
+public class EntityPersistenceExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<EntityPersistenceException> {
+
+ private final UriInfo uriInfo;
+
+ public EntityPersistenceExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final EntityPersistenceException exception) {
+ return fallback(exception, uriInfo);
+ }
+}
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
new file mode 100644
index 0000000..15b367c
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ExceptionMapperBase.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.entitlement.api.BlockingApiException;
+import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.entitlement.api.SubscriptionApiException;
+import org.killbill.billing.entity.EntityPersistenceException;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.jaxrs.json.BillingExceptionJson;
+import org.killbill.billing.overdue.OverdueApiException;
+import org.killbill.billing.payment.api.PaymentApiException;
+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 com.fasterxml.jackson.core.JsonProcessingException;
+
+public abstract class ExceptionMapperBase {
+
+ private static final Logger log = LoggerFactory.getLogger(ExceptionMapperBase.class);
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ protected Response fallback(final Exception exception, final UriInfo uriInfo) {
+ if (exception.getCause() == null) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else {
+ return doFallback(exception, uriInfo);
+ }
+ }
+
+ private Response doFallback(final Exception exception, final UriInfo uriInfo) {
+ if (exception.getCause() == null || !(exception.getCause() instanceof BillingExceptionBase)) {
+ return buildBadRequestResponse(exception, uriInfo);
+ }
+
+ final BillingExceptionBase cause = (BillingExceptionBase) exception.getCause();
+ if (cause instanceof AccountApiException) {
+ final AccountApiExceptionMapper mapper = new AccountApiExceptionMapper(uriInfo);
+ return mapper.toResponse((AccountApiException) cause);
+ } else if (cause instanceof BlockingApiException) {
+ final BlockingApiExceptionMapper mapper = new BlockingApiExceptionMapper(uriInfo);
+ return mapper.toResponse((BlockingApiException) cause);
+ } 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);
+ } else if (cause instanceof EntityPersistenceException) {
+ final EntityPersistenceExceptionMapper mapper = new EntityPersistenceExceptionMapper(uriInfo);
+ return mapper.toResponse((EntityPersistenceException) cause);
+ } else if (cause instanceof InvoiceApiException) {
+ final InvoiceApiExceptionMapper mapper = new InvoiceApiExceptionMapper(uriInfo);
+ return mapper.toResponse((InvoiceApiException) cause);
+ } else if (cause instanceof OverdueApiException) {
+ final OverdueApiExceptionMapper mapper = new OverdueApiExceptionMapper(uriInfo);
+ return mapper.toResponse((OverdueApiException) cause);
+ } else if (cause instanceof PaymentApiException) {
+ final PaymentApiExceptionMapper mapper = new PaymentApiExceptionMapper(uriInfo);
+ return mapper.toResponse((PaymentApiException) cause);
+ } else if (cause instanceof SubscriptionApiException) {
+ final SubscriptionApiExceptionMapper mapper = new SubscriptionApiExceptionMapper(uriInfo);
+ return mapper.toResponse((SubscriptionApiException) cause);
+ } else if (cause instanceof SubscriptionBillingApiException) {
+ final SubscriptionBillingApiExceptionMapper mapper = new SubscriptionBillingApiExceptionMapper(uriInfo);
+ return mapper.toResponse((SubscriptionBillingApiException) cause);
+ } else if (cause instanceof SubscriptionBaseRepairException) {
+ final SubscriptionRepairExceptionMapper mapper = new SubscriptionRepairExceptionMapper(uriInfo);
+ return mapper.toResponse((SubscriptionBaseRepairException) cause);
+ } else if (cause instanceof TagApiException) {
+ final TagApiExceptionMapper mapper = new TagApiExceptionMapper(uriInfo);
+ return mapper.toResponse((TagApiException) cause);
+ } else if (cause instanceof TagDefinitionApiException) {
+ final TagDefinitionApiExceptionMapper mapper = new TagDefinitionApiExceptionMapper(uriInfo);
+ return mapper.toResponse((TagDefinitionApiException) cause);
+ } else {
+ return buildBadRequestResponse(cause, uriInfo);
+ }
+ }
+
+ protected Response buildConflictingRequestResponse(final Exception e, final UriInfo uriInfo) {
+ // Log the full stacktrace
+ log.warn("Conflicting request", e);
+ return buildConflictingRequestResponse(exceptionToString(e), uriInfo);
+ }
+
+ private Response buildConflictingRequestResponse(final String error, final UriInfo uriInfo) {
+ return Response.status(Status.CONFLICT)
+ .entity(error)
+ .type(MediaType.TEXT_PLAIN_TYPE)
+ .build();
+ }
+
+ protected Response buildNotFoundResponse(final Exception e, final UriInfo uriInfo) {
+ // Log the full stacktrace
+ log.info("Not found", e);
+ return buildNotFoundResponse(exceptionToString(e), uriInfo);
+ }
+
+ private Response buildNotFoundResponse(final String error, final UriInfo uriInfo) {
+ return Response.status(Status.NOT_FOUND)
+ .entity(error)
+ .type(MediaType.TEXT_PLAIN_TYPE)
+ .build();
+ }
+
+ protected Response buildBadRequestResponse(final Exception e, final UriInfo uriInfo) {
+ // Log the full stacktrace
+ log.warn("Bad request", e);
+ return buildBadRequestResponse(exceptionToString(e), uriInfo);
+ }
+
+ private Response buildBadRequestResponse(final String error, final UriInfo uriInfo) {
+ return Response.status(Status.BAD_REQUEST)
+ .entity(error)
+ .type(MediaType.TEXT_PLAIN_TYPE)
+ .build();
+ }
+
+ protected Response buildAuthorizationErrorResponse(final Exception e, final UriInfo uriInfo) {
+ // Log the full stacktrace
+ log.warn("Authorization error", e);
+ return buildAuthorizationErrorResponse(exceptionToString(e), uriInfo);
+ }
+
+ private Response buildAuthorizationErrorResponse(final String error, final UriInfo uriInfo) {
+ return Response.status(Status.UNAUTHORIZED) // TODO Forbidden?
+ .entity(error)
+ .type(MediaType.TEXT_PLAIN_TYPE)
+ .build();
+ }
+
+ protected Response buildInternalErrorResponse(final Exception e, final UriInfo uriInfo) {
+ // Log the full stacktrace
+ log.warn("Internal error", e);
+ return buildInternalErrorResponse(exceptionToString(e), uriInfo);
+ }
+
+ private Response buildInternalErrorResponse(final String error, final UriInfo uriInfo) {
+ return Response.status(Status.INTERNAL_SERVER_ERROR)
+ .entity(error)
+ .type(MediaType.TEXT_PLAIN_TYPE)
+ .build();
+ }
+
+ private String exceptionToString(final Exception e) {
+ try {
+ return mapper.writeValueAsString(new BillingExceptionJson(e));
+ } catch (JsonProcessingException jsonException) {
+ log.warn("Unable to serialize exception", jsonException);
+ }
+ return e.toString();
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/IllegalArgumentExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/IllegalArgumentExceptionMapper.java
new file mode 100644
index 0000000..f67a1e2
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/IllegalArgumentExceptionMapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+@Singleton
+@Provider
+public class IllegalArgumentExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<IllegalArgumentException> {
+
+ private final UriInfo uriInfo;
+
+ public IllegalArgumentExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final IllegalArgumentException exception) {
+ // Likely bad UUID String
+ return buildBadRequestResponse(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/IllegalPlanChangeMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/IllegalPlanChangeMapper.java
new file mode 100644
index 0000000..4727990
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/IllegalPlanChangeMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.catalog.api.IllegalPlanChange;
+
+@Singleton
+@Provider
+public class IllegalPlanChangeMapper extends ExceptionMapperBase implements ExceptionMapper<IllegalPlanChange> {
+
+ private final UriInfo uriInfo;
+
+ public IllegalPlanChangeMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final IllegalPlanChange exception) {
+ return buildBadRequestResponse(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/InvoiceApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/InvoiceApiExceptionMapper.java
new file mode 100644
index 0000000..e57921d
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/InvoiceApiExceptionMapper.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+
+@Singleton
+@Provider
+public class InvoiceApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<InvoiceApiException> {
+
+ private final UriInfo uriInfo;
+
+ public InvoiceApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final InvoiceApiException exception) {
+ if (exception.getCode() == ErrorCode.INVOICE_ACCOUNT_ID_INVALID.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_INVALID_DATE_SEQUENCE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_INVALID_TRANSITION.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_NO_ACCOUNT_ID_FOR_SUBSCRIPTION_ID.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_NO_SUCH_CREDIT.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_NOT_FOUND.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_NOTHING_TO_DO.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_PAYMENT_BY_ATTEMPT_NOT_FOUND.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_PAYMENT_NOT_FOUND.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_TARGET_DATE_TOO_FAR_IN_THE_FUTURE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.CREDIT_AMOUNT_INVALID.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_ITEM_ADJUSTMENT_AMOUNT_SHOULD_BE_POSITIVE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_ITEM_NOT_FOUND.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.INVOICE_NO_SUCH_EXTERNAL_CHARGE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.EXTERNAL_CHARGE_AMOUNT_INVALID.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else {
+ return fallback(exception, uriInfo);
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/OverdueApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/OverdueApiExceptionMapper.java
new file mode 100644
index 0000000..869e374
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/OverdueApiExceptionMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.overdue.OverdueApiException;
+
+@Singleton
+@Provider
+public class OverdueApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<OverdueApiException> {
+
+ private final UriInfo uriInfo;
+
+ public OverdueApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final OverdueApiException exception) {
+ return fallback(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/OverdueErrorMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/OverdueErrorMapper.java
new file mode 100644
index 0000000..8ee6366
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/OverdueErrorMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.overdue.config.api.OverdueException;
+
+@Singleton
+@Provider
+public class OverdueErrorMapper extends ExceptionMapperBase implements ExceptionMapper<OverdueException> {
+
+ private final UriInfo uriInfo;
+
+ public OverdueErrorMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final OverdueException exception) {
+ return buildBadRequestResponse(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/PaymentApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/PaymentApiExceptionMapper.java
new file mode 100644
index 0000000..3081d3f
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/PaymentApiExceptionMapper.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.payment.api.PaymentApiException;
+
+@Singleton
+@Provider
+public class PaymentApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<PaymentApiException> {
+
+ private final UriInfo uriInfo;
+
+ public PaymentApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final PaymentApiException exception) {
+ if (exception.getCode() == ErrorCode.PAYMENT_ADD_PAYMENT_METHOD.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_AMOUNT_DENIED.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_BAD_ACCOUNT.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_CREATE_PAYMENT.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_CREATE_PAYMENT_FOR_ATTEMPT.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_CREATE_PAYMENT_FOR_ATTEMPT_BAD.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_CREATE_PAYMENT_FOR_ATTEMPT_WITH_NON_POSITIVE_INV.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_CREATE_PAYMENT_PROVIDER_ACCOUNT.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_CREATE_REFUND.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_DEL_DEFAULT_PAYMENT_METHOD.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_DEL_PAYMENT_METHOD.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_GET_PAYMENT_METHODS.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_GET_PAYMENT_PROVIDER.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_GET_PAYMENT_PROVIDER_ACCOUNT.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_INTERNAL_ERROR.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_NO_PAYMENT_METHODS.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_NO_SUCH_PAYMENT.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_NO_SUCH_REFUND.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_NO_SUCH_SUCCESS_PAYMENT.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_NULL_INVOICE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_PLUGIN_ACCOUNT_INIT.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_PLUGIN_TIMEOUT.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_REFRESH_PAYMENT_METHOD.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_REFUND_AMOUNT_NEGATIVE_OR_NULL.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_REFUND_AMOUNT_TOO_LARGE.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_UPD_GATEWAY_FAILED.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_UPD_PAYMENT_METHOD.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.PAYMENT_UPD_PAYMENT_PROVIDER_ACCOUNT.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else {
+ return fallback(exception, uriInfo);
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/RuntimeExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/RuntimeExceptionMapper.java
new file mode 100644
index 0000000..5117e1f
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/RuntimeExceptionMapper.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+@Provider
+public class RuntimeExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<RuntimeException> {
+
+ private final UriInfo uriInfo;
+
+ private static final Logger log = LoggerFactory.getLogger(RuntimeExceptionMapper.class);
+
+ public RuntimeExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final RuntimeException exception) {
+ if (exception instanceof NullPointerException) {
+ // Assume bad payload
+ exception.printStackTrace();
+ log.warn("Exception : " + exception.getMessage());
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception instanceof WebApplicationException) {
+ // e.g. com.sun.jersey.api.NotFoundException
+ return ((WebApplicationException) exception).getResponse();
+ } else {
+ return buildInternalErrorResponse(exception, uriInfo);
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ShiroExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ShiroExceptionMapper.java
new file mode 100644
index 0000000..b532479
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/ShiroExceptionMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.shiro.ShiroException;
+
+@Singleton
+@Provider
+public class ShiroExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<ShiroException> {
+
+ private final UriInfo uriInfo;
+
+ public ShiroExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final ShiroException exception) {
+ return buildAuthorizationErrorResponse(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionApiExceptionMapper.java
new file mode 100644
index 0000000..ef116df
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionApiExceptionMapper.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.entitlement.api.SubscriptionApiException;
+
+@Singleton
+@Provider
+public class SubscriptionApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<SubscriptionApiException> {
+
+ private final UriInfo uriInfo;
+
+ public SubscriptionApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final SubscriptionApiException exception) {
+ if (exception.getCode() == ErrorCode.SUB_ACCOUNT_IS_OVERDUE_BLOCKED.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_BUNDLE_IS_OVERDUE_BLOCKED.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CANCEL_BAD_STATE.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CHANGE_DRY_RUN_NOT_BP.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CHANGE_FUTURE_CANCELLED.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CHANGE_NON_ACTIVE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CREATE_AO_ALREADY_INCLUDED.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CREATE_AO_BP_NON_ACTIVE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CREATE_AO_NOT_AVAILABLE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CREATE_BAD_PHASE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CREATE_BP_EXISTS.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CREATE_NO_BP.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_CREATE_NO_BUNDLE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_GET_INVALID_BUNDLE_ID.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_GET_INVALID_BUNDLE_KEY.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_GET_NO_BUNDLE_FOR_SUBSCRIPTION.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_INVALID_REQUESTED_DATE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_INVALID_REQUESTED_FUTURE_DATE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_INVALID_SUBSCRIPTION_ID.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_RECREATE_BAD_STATE.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_UNCANCEL_BAD_STATE.getCode()) {
+ return buildInternalErrorResponse(exception, uriInfo);
+ } else {
+ return fallback(exception, uriInfo);
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionBillingApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionBillingApiExceptionMapper.java
new file mode 100644
index 0000000..ce05237
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionBillingApiExceptionMapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.subscription.api.SubscriptionBillingApiException;
+
+@Singleton
+@Provider
+public class SubscriptionBillingApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<SubscriptionBillingApiException> {
+
+ private final UriInfo uriInfo;
+
+ public SubscriptionBillingApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final SubscriptionBillingApiException exception) {
+ return fallback(exception, uriInfo);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionRepairExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionRepairExceptionMapper.java
new file mode 100644
index 0000000..68f99d7
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/SubscriptionRepairExceptionMapper.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseRepairException;
+
+@Singleton
+@Provider
+public class SubscriptionRepairExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<SubscriptionBaseRepairException> {
+
+ private final UriInfo uriInfo;
+
+ public SubscriptionRepairExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final SubscriptionBaseRepairException exception) {
+ if (exception.getCode() == ErrorCode.SUB_REPAIR_AO_CREATE_BEFORE_BP_START.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO_CREATE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_INVALID_DELETE_SET.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_MISSING_AO_DELETE_EVENT.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_AO_REMAINING.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_BP_REMAINING.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_NO_ACTIVE_SUBSCRIPTIONS.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_NON_EXISTENT_DELETE_EVENT.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_SUB_EMPTY.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_SUB_RECREATE_NOT_EMPTY.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_UNKNOWN_BUNDLE.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_UNKNOWN_SUBSCRIPTION.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_UNKNOWN_TYPE.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.SUB_REPAIR_VIEW_CHANGED.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else {
+ return fallback(exception, uriInfo);
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/TagApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/TagApiExceptionMapper.java
new file mode 100644
index 0000000..6cb7e2c
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/TagApiExceptionMapper.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.util.api.TagApiException;
+
+@Singleton
+@Provider
+public class TagApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<TagApiException> {
+
+ private final UriInfo uriInfo;
+
+ public TagApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final TagApiException exception) {
+ if (exception.getCode() == ErrorCode.TAG_DOES_NOT_EXIST.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.TAG_CANNOT_BE_REMOVED.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else {
+ return buildBadRequestResponse(exception, uriInfo);
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/TagDefinitionApiExceptionMapper.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/TagDefinitionApiExceptionMapper.java
new file mode 100644
index 0000000..832741a
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/mappers/TagDefinitionApiExceptionMapper.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.mappers;
+
+import javax.inject.Singleton;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+
+@Singleton
+@Provider
+public class TagDefinitionApiExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<TagDefinitionApiException> {
+
+ private final UriInfo uriInfo;
+
+ public TagDefinitionApiExceptionMapper(@Context final UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ }
+
+ @Override
+ public Response toResponse(final TagDefinitionApiException exception) {
+ if (exception.getCode() == ErrorCode.TAG_DEFINITION_ALREADY_EXISTS.getCode()) {
+ return buildConflictingRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.TAG_DEFINITION_CONFLICTS_WITH_CONTROL_TAG.getCode()) {
+ return buildConflictingRequestResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.TAG_DEFINITION_DOES_NOT_EXIST.getCode()) {
+ return buildNotFoundResponse(exception, uriInfo);
+ } else if (exception.getCode() == ErrorCode.TAG_DEFINITION_IN_USE.getCode()) {
+ return buildBadRequestResponse(exception, uriInfo);
+ } else {
+ return buildBadRequestResponse(exception, uriInfo);
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AuditMode.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AuditMode.java
new file mode 100644
index 0000000..3119172
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AuditMode.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import org.killbill.billing.util.api.AuditLevel;
+
+public class AuditMode {
+
+ private final AuditLevel level;
+
+ public AuditMode(final String auditModeString) {
+ this.level = AuditLevel.valueOf(auditModeString.toUpperCase());
+ }
+
+ public AuditLevel getLevel() {
+ return level;
+ }
+
+ public boolean withAudit() {
+ return !AuditLevel.NONE.equals(level);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("AuditMode");
+ sb.append("{level=").append(level);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final AuditMode auditMode = (AuditMode) o;
+
+ if (level != auditMode.level) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return level != null ? level.hashCode() : 0;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java
new file mode 100644
index 0000000..f3f72a3
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/BundleResource.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.EntitlementApi;
+import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.entitlement.api.SubscriptionApi;
+import org.killbill.billing.entitlement.api.SubscriptionApiException;
+import org.killbill.billing.entitlement.api.SubscriptionBundle;
+import org.killbill.billing.jaxrs.json.BundleJson;
+import org.killbill.billing.jaxrs.json.CustomFieldJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Path(JaxrsResource.BUNDLES_PATH)
+public class BundleResource extends JaxRsResourceBase {
+
+ private static final String ID_PARAM_NAME = "bundleId";
+
+ private final SubscriptionApi subscriptionApi;
+ private final EntitlementApi entitlementApi;
+
+ @Inject
+ public BundleResource(final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final SubscriptionApi subscriptionApi,
+ final EntitlementApi entitlementApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.entitlementApi = entitlementApi;
+ this.subscriptionApi = subscriptionApi;
+ }
+
+ @GET
+ @Path("/{bundleId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response getBundle(@PathParam("bundleId") final String bundleId,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ final UUID id = UUID.fromString(bundleId);
+ final SubscriptionBundle bundle = subscriptionApi.getSubscriptionBundle(id, context.createContext(request));
+ final BundleJson json = new BundleJson(bundle, null);
+ return Response.status(Status.OK).entity(json).build();
+ }
+
+ @GET
+ @Produces(APPLICATION_JSON)
+ public Response getBundleByKey(@QueryParam(QUERY_EXTERNAL_KEY) final String externalKey,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ final SubscriptionBundle bundle = subscriptionApi.getActiveSubscriptionBundleForExternalKey(externalKey, context.createContext(request));
+ final BundleJson json = new BundleJson(bundle, null);
+ return Response.status(Status.OK).entity(json).build();
+ }
+
+ @GET
+ @Path("/" + PAGINATION)
+ @Produces(APPLICATION_JSON)
+ public Response getBundles(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final Pagination<SubscriptionBundle> bundles = subscriptionApi.getSubscriptionBundles(offset, limit, tenantContext);
+ final URI nextPageUri = uriBuilder.nextPage(BundleResource.class, "getBundles", bundles.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_AUDIT, auditMode.getLevel().toString()));
+ final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+ return buildStreamingPaginationResponse(bundles,
+ new Function<SubscriptionBundle, BundleJson>() {
+ @Override
+ public BundleJson apply(final SubscriptionBundle bundle) {
+ // Cache audit logs per account
+ if (accountsAuditLogs.get().get(bundle.getAccountId()) == null) {
+ accountsAuditLogs.get().put(bundle.getAccountId(), auditUserApi.getAccountAuditLogs(bundle.getAccountId(), auditMode.getLevel(), tenantContext));
+ }
+ return new BundleJson(bundle, accountsAuditLogs.get().get(bundle.getAccountId()));
+ }
+ },
+ nextPageUri);
+ }
+
+ @GET
+ @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response searchBundles(@PathParam("searchKey") final String searchKey,
+ @QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final Pagination<SubscriptionBundle> bundles = subscriptionApi.searchSubscriptionBundles(searchKey, offset, limit, tenantContext);
+ final URI nextPageUri = uriBuilder.nextPage(BundleResource.class, "searchBundles", bundles.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+ QUERY_AUDIT, auditMode.getLevel().toString()));
+ final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+ return buildStreamingPaginationResponse(bundles,
+ new Function<SubscriptionBundle, BundleJson>() {
+ @Override
+ public BundleJson apply(final SubscriptionBundle bundle) {
+ // Cache audit logs per account
+ if (accountsAuditLogs.get().get(bundle.getAccountId()) == null) {
+ accountsAuditLogs.get().put(bundle.getAccountId(), auditUserApi.getAccountAuditLogs(bundle.getAccountId(), auditMode.getLevel(), tenantContext));
+ }
+ return new BundleJson(bundle, accountsAuditLogs.get().get(bundle.getAccountId()));
+ }
+ },
+ nextPageUri);
+ }
+
+ @PUT
+ @Path("/{bundleId:" + UUID_PATTERN + "}/" + PAUSE)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response pauseBundle(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
+ @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 SubscriptionApiException, EntitlementApiException {
+
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+ final UUID bundleId = UUID.fromString(id);
+ final SubscriptionBundle bundle = subscriptionApi.getSubscriptionBundle(bundleId, callContext);
+ final LocalDate inputLocalDate = toLocalDate(bundle.getAccountId(), requestedDate, callContext);
+ entitlementApi.pause(bundleId, inputLocalDate, callContext);
+ return Response.status(Status.OK).build();
+ }
+
+ @PUT
+ @Path("/{bundleId:" + UUID_PATTERN + "}/" + RESUME)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response resumeBundle(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
+ @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 SubscriptionApiException, EntitlementApiException {
+
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+ final UUID bundleId = UUID.fromString(id);
+ final SubscriptionBundle bundle = subscriptionApi.getSubscriptionBundle(bundleId, callContext);
+ final LocalDate inputLocalDate = toLocalDate(bundle.getAccountId(), requestedDate, callContext);
+ entitlementApi.resume(bundleId, inputLocalDate, callContext);
+ return Response.status(Status.OK).build();
+ }
+
+ @GET
+ @Path("/{bundleId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Produces(APPLICATION_JSON)
+ public Response getCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) {
+ return super.getCustomFields(UUID.fromString(id), auditMode, context.createContext(request));
+ }
+
+ @POST
+ @Path("/{bundleId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ final List<CustomFieldJson> customFields,
+ @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,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws CustomFieldApiException {
+ return super.createCustomFields(UUID.fromString(id), customFields,
+ context.createContext(createdBy, reason, comment, request), uriInfo);
+ }
+
+ @DELETE
+ @Path("/{bundleId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response deleteCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_CUSTOM_FIELDS) final String customFieldList,
+ @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 CustomFieldApiException {
+ return super.deleteCustomFields(UUID.fromString(id), customFieldList,
+ context.createContext(createdBy, reason, comment, request));
+ }
+
+ @GET
+ @Path("/{bundleId:" + UUID_PATTERN + "}/" + TAGS)
+ @Produces(APPLICATION_JSON)
+ public Response getTags(@PathParam(ID_PARAM_NAME) final String bundleIdString,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @QueryParam(QUERY_TAGS_INCLUDED_DELETED) @DefaultValue("false") final Boolean includedDeleted,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException, SubscriptionApiException {
+ final UUID bundleId = UUID.fromString(bundleIdString);
+ final TenantContext tenantContext = context.createContext(request);
+ final SubscriptionBundle bundle = subscriptionApi.getSubscriptionBundle(bundleId, context.createContext(request));
+ return super.getTags(bundle.getAccountId(), bundleId, auditMode, includedDeleted, tenantContext);
+ }
+
+ @PUT
+ @Path("/{bundleId:" + UUID_PATTERN + "}")
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response transferBundle(final BundleJson json,
+ @PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
+ @QueryParam(QUERY_BILLING_POLICY) @DefaultValue("END_OF_TERM") final String policyString,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final UriInfo uriInfo,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws EntitlementApiException, SubscriptionApiException, AccountApiException {
+
+ final BillingActionPolicy policy = BillingActionPolicy.valueOf(policyString.toUpperCase());
+
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+ final UUID bundleId = UUID.fromString(id);
+
+ final SubscriptionBundle bundle = subscriptionApi.getSubscriptionBundle(bundleId, callContext);
+ final LocalDate inputLocalDate = toLocalDate(bundle.getAccountId(), requestedDate, callContext);
+
+ final UUID newBundleId = entitlementApi.transferEntitlementsOverrideBillingPolicy(bundle.getAccountId(), UUID.fromString(json.getAccountId()), bundle.getExternalKey(), inputLocalDate, policy, callContext);
+ return uriBuilder.buildResponse(BundleResource.class, "getBundle", newBundleId, uriInfo.getBaseUri().toString());
+ }
+
+ @POST
+ @Path("/{bundleId:" + UUID_PATTERN + "}/" + TAGS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createTags(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_TAGS) final String tagList,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final UriInfo uriInfo,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagApiException {
+ return super.createTags(UUID.fromString(id), tagList, uriInfo,
+ context.createContext(createdBy, reason, comment, request));
+ }
+
+ @DELETE
+ @Path("/{bundleId:" + UUID_PATTERN + "}/" + TAGS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response deleteTags(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_TAGS) final String tagList,
+ @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 TagApiException {
+ return super.deleteTags(UUID.fromString(id), tagList,
+ context.createContext(createdBy, reason, comment, request));
+ }
+
+ @Override
+ protected ObjectType getObjectType() {
+ return ObjectType.BUNDLE;
+ }
+}
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
new file mode 100644
index 0000000..21ca4ec
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.Listing;
+import org.killbill.billing.catalog.api.StaticCatalog;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.json.CatalogJsonSimple;
+import org.killbill.billing.jaxrs.json.PlanDetailJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.config.catalog.XMLWriter;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+import static javax.ws.rs.core.MediaType.APPLICATION_XML;
+
+@Singleton
+@Path(JaxrsResource.CATALOG_PATH)
+public class CatalogResource extends JaxRsResourceBase {
+
+ private final CatalogService catalogService;
+
+ @Inject
+ public CatalogResource(final CatalogService catalogService,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.catalogService = catalogService;
+ }
+
+ @GET
+ @Produces(APPLICATION_XML)
+ public Response getCatalogXml(@javax.ws.rs.core.Context final HttpServletRequest request) throws Exception {
+ return Response.status(Status.OK).entity(XMLWriter.writeXML(catalogService.getCurrentCatalog(), StaticCatalog.class)).build();
+ }
+
+ @GET
+ @Produces(APPLICATION_JSON)
+ public Response getCatalogJson(@javax.ws.rs.core.Context final HttpServletRequest request) throws Exception {
+ final StaticCatalog catalog = catalogService.getCurrentCatalog();
+
+ return Response.status(Status.OK).entity(catalog).build();
+ }
+
+ // Need to figure out dependency on StandaloneCatalog
+ // @GET
+ // @Path("/xsd")
+ // @Produces(APPLICATION_XML)
+ // public String getCatalogXsd() throws Exception
+ // {
+ // InputStream stream = XMLSchemaGenerator.xmlSchema(StandaloneCatalog.class);
+ // StringWriter writer = new StringWriter();
+ // IOUtils.copy(stream, writer);
+ // String result = writer.toString();
+ //
+ // return result;
+ // }
+
+ @GET
+ @Path("/availableAddons")
+ @Produces(APPLICATION_JSON)
+ public Response getAvailableAddons(@QueryParam("baseProductName") final String baseProductName,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws CatalogApiException {
+ final StaticCatalog catalog = catalogService.getCurrentCatalog();
+ final List<Listing> listings = catalog.getAvailableAddonListings(baseProductName);
+ final List<PlanDetailJson> details = new ArrayList<PlanDetailJson>();
+ for (final Listing listing : listings) {
+ details.add(new PlanDetailJson(listing));
+ }
+ return Response.status(Status.OK).entity(details).build();
+ }
+
+ @GET
+ @Path("/availableBasePlans")
+ @Produces(APPLICATION_JSON)
+ public Response getAvailableBasePlans(@javax.ws.rs.core.Context final HttpServletRequest request) throws CatalogApiException {
+ final StaticCatalog catalog = catalogService.getCurrentCatalog();
+ final List<Listing> listings = catalog.getAvailableBasePlanListings();
+ final List<PlanDetailJson> details = new ArrayList<PlanDetailJson>();
+ for (final Listing listing : listings) {
+ details.add(new PlanDetailJson(listing));
+ }
+ return Response.status(Status.OK).entity(details).build();
+ }
+
+ @GET
+ @Path("/simpleCatalog")
+ @Produces(APPLICATION_JSON)
+ public Response getSimpleCatalog(@javax.ws.rs.core.Context final HttpServletRequest request) throws CatalogApiException {
+ final StaticCatalog catalog = catalogService.getCurrentCatalog();
+
+ final CatalogJsonSimple json = new CatalogJsonSimple(catalog);
+ return Response.status(Status.OK).entity(json).build();
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/ChargebackResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/ChargebackResource.java
new file mode 100644
index 0000000..4145098
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/ChargebackResource.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentApi;
+import org.killbill.billing.jaxrs.json.ChargebackJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.CHARGEBACKS_PATH)
+public class ChargebackResource extends JaxRsResourceBase {
+
+ private final InvoicePaymentApi invoicePaymentApi;
+
+ @Inject
+ public ChargebackResource(final InvoicePaymentApi invoicePaymentApi,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.invoicePaymentApi = invoicePaymentApi;
+ }
+
+ @GET
+ @Path("/{chargebackId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response getChargeback(@PathParam("chargebackId") final String chargebackId,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws InvoiceApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final InvoicePayment chargeback = invoicePaymentApi.getChargebackById(UUID.fromString(chargebackId), tenantContext);
+ final UUID accountId = invoicePaymentApi.getAccountIdFromInvoicePaymentId(chargeback.getId(), tenantContext);
+ final ChargebackJson chargebackJson = new ChargebackJson(accountId, chargeback);
+
+ return Response.status(Response.Status.OK).entity(chargebackJson).build();
+ }
+
+
+
+ @POST
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createChargeback(final ChargebackJson json,
+ @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,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws InvoiceApiException {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+ final InvoicePayment invoicePayment = invoicePaymentApi.getInvoicePaymentForAttempt(UUID.fromString(json.getPaymentId()), callContext);
+ if (invoicePayment == null) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_PAYMENT_NOT_FOUND, json.getPaymentId());
+ }
+ final InvoicePayment chargeBack = invoicePaymentApi.createChargeback(invoicePayment.getId(), json.getAmount(),
+ callContext);
+ return uriBuilder.buildResponse(uriInfo, ChargebackResource.class, "getChargeback", chargeBack.getId());
+ }
+
+ @Override
+ protected ObjectType getObjectType() {
+ return ObjectType.INVOICE_PAYMENT;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CreditResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CreditResource.java
new file mode 100644
index 0000000..d570776
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CreditResource.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.joda.time.LocalDate;
+
+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.AccountUserApi;
+import org.killbill.clock.Clock;
+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.InvoiceUserApi;
+import org.killbill.billing.jaxrs.json.CreditJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.CREDITS_PATH)
+public class CreditResource extends JaxRsResourceBase {
+
+ private final InvoiceUserApi invoiceUserApi;
+ private final AccountUserApi accountUserApi;
+
+ @Inject
+ public CreditResource(final InvoiceUserApi invoiceUserApi,
+ final AccountUserApi accountUserApi,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.invoiceUserApi = invoiceUserApi;
+ this.accountUserApi = accountUserApi;
+ }
+
+ @GET
+ @Path("/{creditId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response getCredit(@PathParam("creditId") final String creditId,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws InvoiceApiException, AccountApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final InvoiceItem credit = invoiceUserApi.getCreditById(UUID.fromString(creditId), tenantContext);
+ final Invoice invoice = invoiceUserApi.getInvoice(credit.getInvoiceId(), tenantContext);
+ final CreditJson creditJson = new CreditJson(invoice, credit);
+ return Response.status(Response.Status.OK).entity(creditJson).build();
+ }
+
+ @POST
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createCredit(final CreditJson json,
+ @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,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws AccountApiException, InvoiceApiException {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+ final Account account = accountUserApi.getAccountById(UUID.fromString(json.getAccountId()), callContext);
+ final LocalDate effectiveDate = new LocalDate(clock.getUTCNow(), account.getTimeZone());
+
+ final InvoiceItem credit;
+ if (json.getInvoiceId() != null) {
+ // Apply an invoice level credit
+ credit = invoiceUserApi.insertCreditForInvoice(account.getId(), UUID.fromString(json.getInvoiceId()), json.getCreditAmount(),
+ effectiveDate, account.getCurrency(), callContext);
+ } else {
+ // Apply a account level credit
+ credit = invoiceUserApi.insertCredit(account.getId(), json.getCreditAmount(), effectiveDate,
+ account.getCurrency(), callContext);
+ }
+
+ return uriBuilder.buildResponse(uriInfo, CreditResource.class, "getCredit", credit.getId());
+ }
+
+ @Override
+ protected ObjectType getObjectType() {
+ return ObjectType.INVOICE_ITEM;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CustomFieldResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CustomFieldResource.java
new file mode 100644
index 0000000..683240a
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CustomFieldResource.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.net.URI;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.json.CustomFieldJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.customfield.CustomField;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.CUSTOM_FIELDS_PATH)
+public class CustomFieldResource extends JaxRsResourceBase {
+
+ @Inject
+ public CustomFieldResource(final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ }
+
+ @GET
+ @Path("/" + PAGINATION)
+ @Produces(APPLICATION_JSON)
+ public Response getCustomFields(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws CustomFieldApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final Pagination<CustomField> customFields = customFieldUserApi.getCustomFields(offset, limit, tenantContext);
+ final URI nextPageUri = uriBuilder.nextPage(CustomFieldResource.class, "getCustomFields", customFields.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_AUDIT, auditMode.getLevel().toString()));
+
+ return buildStreamingPaginationResponse(customFields,
+ new Function<CustomField, CustomFieldJson>() {
+ @Override
+ public CustomFieldJson apply(final CustomField customField) {
+ // TODO Really slow - we should instead try to figure out the account id
+ final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(customField.getId(), ObjectType.CUSTOM_FIELD, auditMode.getLevel(), tenantContext);
+ return new CustomFieldJson(customField, auditLogs);
+ }
+ },
+ nextPageUri);
+ }
+
+ @GET
+ @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response searchCustomFields(@PathParam("searchKey") final String searchKey,
+ @QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws CustomFieldApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final Pagination<CustomField> customFields = customFieldUserApi.searchCustomFields(searchKey, offset, limit, tenantContext);
+ final URI nextPageUri = uriBuilder.nextPage(CustomFieldResource.class, "searchCustomFields", customFields.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+ QUERY_AUDIT, auditMode.getLevel().toString()));
+ return buildStreamingPaginationResponse(customFields,
+ new Function<CustomField, CustomFieldJson>() {
+ @Override
+ public CustomFieldJson apply(final CustomField customField) {
+ // TODO Really slow - we should instead try to figure out the account id
+ final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(customField.getId(), ObjectType.CUSTOM_FIELD, auditMode.getLevel(), tenantContext);
+ return new CustomFieldJson(customField, auditLogs);
+ }
+ },
+ nextPageUri);
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/ExportResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/ExportResource.java
new file mode 100644
index 0000000..ffe7334
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/ExportResource.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.UUID;
+
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.StreamingOutput;
+
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.ExportUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.callcontext.CallContext;
+
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
+
+@Singleton
+@Path(JaxrsResource.EXPORT_PATH)
+public class ExportResource extends JaxRsResourceBase {
+
+ private final ExportUserApi exportUserApi;
+
+ @Inject
+ public ExportResource(final ExportUserApi exportUserApi,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.exportUserApi = exportUserApi;
+ }
+
+ @GET
+ @Path("/{accountId:" + UUID_PATTERN + "}")
+ @Produces(TEXT_PLAIN)
+ public StreamingOutput exportDataForAccount(@PathParam("accountId") final String accountId,
+ @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) {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+ return new StreamingOutput() {
+ @Override
+ public void write(final OutputStream output) throws IOException, WebApplicationException {
+ // CSV by default for now
+ exportUserApi.exportDataAsCSVForAccount(UUID.fromString(accountId), output, callContext);
+ }
+ };
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
new file mode 100644
index 0000000..27e781a
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+public interface JaxrsResource {
+
+ public static final String API_PREFIX = "";
+ public static final String API_VERSION = "/1.0";
+ public static final String API_POSTFIX = "/kb";
+
+ public static final String PREFIX = API_PREFIX + API_VERSION + API_POSTFIX;
+
+ public static final String TIMELINE = "timeline";
+ public static final String REGISTER_NOTIFICATION_CALLBACK = "registerNotificationCallback";
+ public static final String SEARCH = "search";
+
+ /*
+ * Multi-Tenancy headers
+ */
+ public static String HDR_API_KEY = "X-Killbill-ApiKey";
+ public static String HDR_API_SECRET = "X-Killbill-ApiSecret";
+
+ /*
+ * Metadata Additional headers
+ */
+ public static String HDR_CREATED_BY = "X-Killbill-CreatedBy";
+ public static String HDR_REASON = "X-Killbill-Reason";
+ public static String HDR_COMMENT = "X-Killbill-Comment";
+ public static String HDR_PAGINATION_CURRENT_OFFSET = "X-Killbill-Pagination-CurrentOffset";
+ public static String HDR_PAGINATION_NEXT_OFFSET = "X-Killbill-Pagination-NextOffset";
+ public static String HDR_PAGINATION_TOTAL_NB_RECORDS = "X-Killbill-Pagination-TotalNbRecords";
+ public static String HDR_PAGINATION_MAX_NB_RECORDS = "X-Killbill-Pagination-MaxNbRecords";
+ public static String HDR_PAGINATION_NEXT_PAGE_URI = "X-Killbill-Pagination-NextPageUri";
+
+ /*
+ * Patterns
+ */
+ public static String STRING_PATTERN = "[\\w-]+";
+ public static String UUID_PATTERN = "\\w+-\\w+-\\w+-\\w+-\\w+";
+ public static String NUMBER_PATTERN = "[0-9]+";
+ public static String ANYTHING_PATTERN = ".*";
+
+ /*
+ * Query parameters
+ */
+ public static final String QUERY_EXTERNAL_KEY = "externalKey";
+ public static final String QUERY_API_KEY = "apiKey";
+ public static final String QUERY_REQUESTED_DT = "requestedDate";
+ public static final String QUERY_CALL_COMPLETION = "callCompletion";
+ public static final String QUERY_USE_REQUESTED_DATE_FOR_BILLING = "useRequestedDateForBilling";
+ public static final String QUERY_CALL_TIMEOUT = "callTimeoutSec";
+ public static final String QUERY_DRY_RUN = "dryRun";
+ public static final String QUERY_TARGET_DATE = "targetDate";
+ public static final String QUERY_BILLING_POLICY = "billingPolicy";
+ public static final String QUERY_ENTITLEMENT_POLICY = "entitlementPolicy";
+ public static final String QUERY_SEARCH_OFFSET = "offset";
+ public static final String QUERY_SEARCH_LIMIT = "limit";
+
+ public static final String QUERY_ACCOUNT_WITH_BALANCE = "accountWithBalance";
+ public static final String QUERY_ACCOUNT_WITH_BALANCE_AND_CBA = "accountWithBalanceAndCBA";
+
+ public static final String QUERY_ACCOUNT_ID = "accountId";
+
+ public static final String QUERY_INVOICE_WITH_ITEMS = "withItems";
+ public static final String QUERY_UNPAID_INVOICES_ONLY = "unpaidInvoicesOnly";
+
+ public static final String QUERY_PAYMENT_EXTERNAL = "externalPayment";
+ public static final String QUERY_PAYMENT_WITH_REFUNDS_AND_CHARGEBACKS = "withRefundsAndChargebacks";
+ public static final String QUERY_PAYMENT_PLUGIN_NAME = "pluginName";
+
+ public static final String QUERY_TAGS = "tagList";
+ public static final String QUERY_TAGS_INCLUDED_DELETED = "includedDeleted";
+ public static final String QUERY_CUSTOM_FIELDS = "customFieldList";
+
+ public static final String QUERY_PAYMENT_METHOD_PLUGIN_NAME = "pluginName";
+ public static final String QUERY_PAYMENT_METHOD_PLUGIN_INFO = "withPluginInfo";
+ public static final String QUERY_PAYMENT_METHOD_IS_DEFAULT = "isDefault";
+
+ public static final String QUERY_PAY_ALL_UNPAID_INVOICES = "payAllUnpaidInvoices";
+ public static final String QUERY_PAY_INVOICE = "payInvoice";
+
+ public static final String QUERY_BUNDLE_TRANSFER_ADDON = "transferAddOn";
+ public static final String QUERY_BUNDLE_TRANSFER_CANCEL_IMM = "cancelImmediately";
+
+ public static final String QUERY_DELETE_DEFAULT_PM_WITH_AUTO_PAY_OFF = "deleteDefaultPmWithAutoPayOff";
+
+ public static final String QUERY_AUDIT = "audit";
+
+ public static final String QUERY_NOTIFICATION_CALLBACK = "cb";
+
+ public static final String PAGINATION = "pagination";
+
+ public static final String ACCOUNTS = "accounts";
+ public static final String ACCOUNTS_PATH = PREFIX + "/" + ACCOUNTS;
+
+ public static final String ANALYTICS = "analytics";
+ public static final String ANALYTICS_PATH = PREFIX + "/" + ANALYTICS;
+
+ public static final String BUNDLES = "bundles";
+ public static final String BUNDLES_PATH = PREFIX + "/" + BUNDLES;
+
+ public static final String SECURITY = "security";
+ public static final String SECURITY_PATH = PREFIX + "/" + SECURITY;
+
+ public static final String SUBSCRIPTIONS = "subscriptions";
+ public static final String SUBSCRIPTIONS_PATH = PREFIX + "/" + SUBSCRIPTIONS;
+
+ public static final String ENTITLEMENTS = "entitlements";
+ public static final String ENTITLEMENTS_PATH = PREFIX + "/" + ENTITLEMENTS;
+
+ public static final String TAG_DEFINITIONS = "tagDefinitions";
+ public static final String TAG_DEFINITIONS_PATH = PREFIX + "/" + TAG_DEFINITIONS;
+
+ public static final String INVOICES = "invoices";
+ public static final String INVOICES_PATH = PREFIX + "/" + INVOICES;
+
+ public static final String CHARGES = "charges";
+ public static final String CHARGES_PATH = PREFIX + "/" + INVOICES + "/" + CHARGES;
+
+ public static final String PAYMENTS = "payments";
+ public static final String PAYMENTS_PATH = PREFIX + "/" + PAYMENTS;
+
+ public static final String REFUNDS = "refunds";
+ public static final String REFUNDS_PATH = PREFIX + "/" + "refunds";
+
+ public static final String PAYMENT_METHODS = "paymentMethods";
+ public static final String PAYMENT_METHODS_PATH = PREFIX + "/" + PAYMENT_METHODS;
+ public static final String PAYMENT_METHODS_DEFAULT_PATH_POSTFIX = "setDefault";
+
+ public static final String CREDITS = "credits";
+ public static final String CREDITS_PATH = PREFIX + "/" + CREDITS;
+
+ public static final String CHARGEBACKS = "chargebacks";
+ public static final String CHARGEBACKS_PATH = PREFIX + "/" + CHARGEBACKS;
+
+ public static final String TAGS = "tags";
+ public static final String TAGS_PATH = PREFIX + "/" + TAGS;
+
+ public static final String CUSTOM_FIELDS = "customFields";
+ public static final String CUSTOM_FIELDS_PATH = PREFIX + "/" + CUSTOM_FIELDS;
+
+ public static final String EMAILS = "emails";
+ public static final String EMAIL_NOTIFICATIONS = "emailNotifications";
+
+ public static final String CATALOG = "catalog";
+ public static final String CATALOG_PATH = PREFIX + "/" + CATALOG;
+
+ public static final String OVERDUE = "overdue";
+ public static final String OVERDUE_PATH = PREFIX + "/" + OVERDUE;
+
+ public static final String TENANTS = "tenants";
+ public static final String TENANTS_PATH = PREFIX + "/" + TENANTS;
+
+ public static final String EXPORT = "export";
+ public static final String EXPORT_PATH = PREFIX + "/" + EXPORT;
+
+ public static final String PLUGINS = "plugins";
+ // No PREFIX here!
+ public static final String PLUGINS_PATH = "/" + PLUGINS;
+
+ public static final String CBA_REBALANCING = "cbaRebalancing";
+
+ public static final String PAUSE = "pause";
+ public static final String RESUME = "resume";
+
+}
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
new file mode 100644
index 0000000..44d66cb
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.StreamingOutput;
+import javax.ws.rs.core.UriInfo;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.json.CustomFieldJson;
+import org.killbill.billing.jaxrs.json.JsonBase;
+import org.killbill.billing.jaxrs.json.TagJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.audit.AccountAuditLogsForObjectType;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.customfield.CustomField;
+import org.killbill.billing.util.customfield.StringCustomField;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.tag.Tag;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public abstract class JaxRsResourceBase implements JaxrsResource {
+
+ static final Logger log = LoggerFactory.getLogger(JaxRsResourceBase.class);
+
+ protected static final ObjectMapper mapper = new ObjectMapper();
+
+ protected final JaxrsUriBuilder uriBuilder;
+ protected final TagUserApi tagUserApi;
+ protected final CustomFieldUserApi customFieldUserApi;
+ protected final AuditUserApi auditUserApi;
+ protected final AccountUserApi accountUserApi;
+ protected final Context context;
+ protected final Clock clock;
+
+ protected final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTimeParser();
+ protected final DateTimeFormatter LOCAL_DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd");
+
+ public JaxRsResourceBase(final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ this.uriBuilder = uriBuilder;
+ this.tagUserApi = tagUserApi;
+ this.customFieldUserApi = customFieldUserApi;
+ this.auditUserApi = auditUserApi;
+ this.accountUserApi = accountUserApi;
+ this.clock = clock;
+ this.context = context;
+ }
+
+ protected ObjectType getObjectType() {
+ return null;
+ }
+
+ protected Response getTags(final UUID accountId, final UUID taggedObjectId, final AuditMode auditMode, final boolean includeDeleted, final TenantContext context) throws TagDefinitionApiException {
+ final List<Tag> tags = tagUserApi.getTagsForObject(taggedObjectId, getObjectType(), includeDeleted, context);
+ final AccountAuditLogsForObjectType tagsAuditLogs = auditUserApi.getAccountAuditLogs(accountId, ObjectType.TAG, auditMode.getLevel(), context);
+
+ final Map<UUID, TagDefinition> tagDefinitionsCache = new HashMap<UUID, TagDefinition>();
+ final Collection<TagJson> result = new LinkedList<TagJson>();
+ for (final Tag tag : tags) {
+ if (tagDefinitionsCache.get(tag.getTagDefinitionId()) == null) {
+ tagDefinitionsCache.put(tag.getTagDefinitionId(), tagUserApi.getTagDefinition(tag.getTagDefinitionId(), context));
+ }
+ final TagDefinition tagDefinition = tagDefinitionsCache.get(tag.getTagDefinitionId());
+
+ final List<AuditLog> auditLogs = tagsAuditLogs.getAuditLogs(tag.getId());
+ result.add(new TagJson(tag, tagDefinition, auditLogs));
+ }
+
+ return Response.status(Response.Status.OK).entity(result).build();
+ }
+
+ protected Response createTags(final UUID id,
+ final String tagList,
+ final UriInfo uriInfo,
+ final CallContext context) throws TagApiException {
+ final Collection<UUID> input = getTagDefinitionUUIDs(tagList);
+ tagUserApi.addTags(id, getObjectType(), input, context);
+ // TODO This will always return 201, even if some (or all) tags already existed (in which case we don't do anything)
+ return uriBuilder.buildResponse(this.getClass(), "getTags", id, uriInfo.getBaseUri().toString());
+ }
+
+ protected Collection<UUID> getTagDefinitionUUIDs(final String tagList) {
+ final String[] tagParts = tagList.split(",\\s*");
+ return Collections2.transform(ImmutableList.copyOf(tagParts), new Function<String, UUID>() {
+ @Override
+ public UUID apply(final String input) {
+ return UUID.fromString(input);
+ }
+ });
+ }
+
+ protected Response deleteTags(final UUID id,
+ final String tagList,
+ final CallContext context) throws TagApiException {
+ final Collection<UUID> input = getTagDefinitionUUIDs(tagList);
+ tagUserApi.removeTags(id, getObjectType(), input, context);
+
+ return Response.status(Response.Status.OK).build();
+ }
+
+ protected Response getCustomFields(final UUID id, final AuditMode auditMode, final TenantContext context) {
+ final List<CustomField> fields = customFieldUserApi.getCustomFieldsForObject(id, getObjectType(), context);
+
+ final List<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);
+ result.add(new CustomFieldJson(cur, auditLogs));
+ }
+
+ return Response.status(Response.Status.OK).entity(result).build();
+ }
+
+ protected Response createCustomFields(final UUID id,
+ final List<CustomFieldJson> customFields,
+ final CallContext context,
+ final UriInfo uriInfo) throws CustomFieldApiException {
+ final LinkedList<CustomField> input = new LinkedList<CustomField>();
+ for (final CustomFieldJson cur : customFields) {
+ input.add(new StringCustomField(cur.getName(), cur.getValue(), getObjectType(), id, context.getCreatedDate()));
+ }
+
+ customFieldUserApi.addCustomFields(input, context);
+ return uriBuilder.buildResponse(uriInfo, this.getClass(), "getCustomFields", id);
+ }
+
+ /**
+ * @param id the if of the object for which the custom fields apply
+ * @param customFieldList a comma separated list of custom field ids or null if they should all be removed
+ * @param context the context
+ * @return
+ * @throws CustomFieldApiException
+ */
+ protected Response deleteCustomFields(final UUID id,
+ @Nullable final String customFieldList,
+ final CallContext context) throws CustomFieldApiException {
+
+ // Retrieve all the custom fields for the object
+ final List<CustomField> fields = customFieldUserApi.getCustomFieldsForObject(id, getObjectType(), context);
+
+ final String[] requestedIds = customFieldList != null ? customFieldList.split("\\s*,\\s*") : null;
+
+ // Filter the proposed list to only keep the one that exist and indeed match our object
+ final Iterable inputIterable = Iterables.filter(fields, new Predicate<CustomField>() {
+ @Override
+ public boolean apply(final CustomField input) {
+ if (customFieldList == null) {
+ return true;
+ }
+ for (final String cur : requestedIds) {
+ final UUID curId = UUID.fromString(cur);
+ if (input.getId().equals(curId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+
+ if (inputIterable.iterator().hasNext()) {
+ final List<CustomField> input = ImmutableList.<CustomField>copyOf(inputIterable);
+ customFieldUserApi.removeCustomFields(input, context);
+ }
+ return Response.status(Response.Status.OK).build();
+ }
+
+ protected <E extends Entity, J extends JsonBase> Response buildStreamingPaginationResponse(final Pagination<E> entities,
+ final Function<E, J> toJson,
+ final URI nextPageUri) {
+ final StreamingOutput json = new StreamingOutput() {
+ @Override
+ public void write(final OutputStream output) throws IOException, WebApplicationException {
+ final JsonGenerator generator = mapper.getFactory().createJsonGenerator(output);
+ generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+
+ generator.writeStartArray();
+ for (final E entity : entities) {
+ generator.writeObject(toJson.apply(entity));
+ }
+ generator.writeEndArray();
+ generator.close();
+ }
+ };
+
+ return Response.status(Status.OK)
+ .entity(json)
+ .header(HDR_PAGINATION_CURRENT_OFFSET, entities.getCurrentOffset())
+ .header(HDR_PAGINATION_NEXT_OFFSET, entities.getNextOffset())
+ .header(HDR_PAGINATION_TOTAL_NB_RECORDS, entities.getTotalNbRecords())
+ .header(HDR_PAGINATION_MAX_NB_RECORDS, entities.getMaxNbRecords())
+ .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
+ .build();
+ }
+
+ protected LocalDate toLocalDate(final UUID accountId, final String inputDate, final TenantContext context) {
+
+ final LocalDate maybeResult = extractLocalDate(inputDate);
+ if (maybeResult != null) {
+ return maybeResult;
+ }
+ Account account = null;
+ try {
+ account = accountId != null ? accountUserApi.getAccountById(accountId, context) : null;
+ } catch (AccountApiException e) {
+ log.info("Failed to retrieve account for id " + accountId);
+ }
+ final DateTime inputDateTime = inputDate != null ? DATE_TIME_FORMATTER.parseDateTime(inputDate) : clock.getUTCNow();
+ return toLocalDate(account, inputDateTime, context);
+ }
+
+ protected LocalDate toLocalDate(final Account account, final String inputDate, final TenantContext context) {
+
+ final LocalDate maybeResult = extractLocalDate(inputDate);
+ if (maybeResult != null) {
+ return maybeResult;
+ }
+ final DateTime inputDateTime = inputDate != null ? DATE_TIME_FORMATTER.parseDateTime(inputDate) : clock.getUTCNow();
+ return toLocalDate(account, inputDateTime, context);
+ }
+
+ private LocalDate toLocalDate(final Account account, final DateTime inputDate, final TenantContext context) {
+ if (account == null && inputDate == null) {
+ // We have no inputDate and so accountTimeZone so we default to LocalDate as seen in UTC
+ return new LocalDate(clock.getUTCNow(), DateTimeZone.UTC);
+ } else if (account == null && inputDate != null) {
+ // We were given a date but can't get timezone, default in UTC
+ return new LocalDate(inputDate, DateTimeZone.UTC);
+ } else if (account != null && inputDate == null) {
+ // We have no inputDate but for accountTimeZone so default to LocalDate as seen in account timezone
+ return new LocalDate(clock.getUTCNow(), account.getTimeZone());
+ } else {
+ // Precise LocalDate as requested
+ return new LocalDate(inputDate, account.getTimeZone());
+ }
+ }
+
+ private LocalDate extractLocalDate(final String inputDate) {
+ if (inputDate != null) {
+ try {
+ final LocalDate localDate = LocalDate.parse(inputDate, LOCAL_DATE_FORMATTER);
+ return localDate;
+ } catch (IllegalArgumentException expectedAndIgnore) {
+ }
+ }
+ return null;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentMethodResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentMethodResource.java
new file mode 100644
index 0000000..e181224
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentMethodResource.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+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.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.json.PaymentMethodJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentMethod;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.PAYMENT_METHODS_PATH)
+public class PaymentMethodResource extends JaxRsResourceBase {
+
+ private final PaymentApi paymentApi;
+
+ @Inject
+ public PaymentMethodResource(final PaymentApi paymentApi,
+ final AccountUserApi accountUserApi,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.paymentApi = paymentApi;
+ }
+
+ @GET
+ @Path("/{paymentMethodId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response getPaymentMethod(@PathParam("paymentMethodId") final String paymentMethodId,
+ @QueryParam(QUERY_PAYMENT_METHOD_PLUGIN_INFO) @DefaultValue("false") final Boolean withPluginInfo,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, PaymentApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ final PaymentMethod paymentMethod = paymentApi.getPaymentMethodById(UUID.fromString(paymentMethodId), false, withPluginInfo, tenantContext);
+ final Account account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
+ final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(paymentMethod.getAccountId(), auditMode.getLevel(), tenantContext);
+ final PaymentMethodJson json = PaymentMethodJson.toPaymentMethodJson(account, paymentMethod, accountAuditLogs);
+
+ return Response.status(Status.OK).entity(json).build();
+ }
+
+ @GET
+ @Path("/" + PAGINATION)
+ @Produces(APPLICATION_JSON)
+ public Response getPaymentMethods(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_PAYMENT_METHOD_PLUGIN_NAME) final String pluginName,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ final Pagination<PaymentMethod> paymentMethods;
+ if (Strings.isNullOrEmpty(pluginName)) {
+ paymentMethods = paymentApi.getPaymentMethods(offset, limit, tenantContext);
+ } else {
+ paymentMethods = paymentApi.getPaymentMethods(offset, limit, pluginName, tenantContext);
+ }
+
+ final URI nextPageUri = uriBuilder.nextPage(PaymentMethodResource.class, "getPaymentMethods", paymentMethods.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_PAYMENT_METHOD_PLUGIN_NAME, Strings.nullToEmpty(pluginName),
+ QUERY_AUDIT, auditMode.getLevel().toString()));
+
+ final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+ final Map<UUID, Account> accounts = new HashMap<UUID, Account>();
+ return buildStreamingPaginationResponse(paymentMethods,
+ new Function<PaymentMethod, PaymentMethodJson>() {
+ @Override
+ public PaymentMethodJson apply(final PaymentMethod paymentMethod) {
+ // Cache audit logs per account
+ if (accountsAuditLogs.get().get(paymentMethod.getAccountId()) == null) {
+ accountsAuditLogs.get().put(paymentMethod.getAccountId(), auditUserApi.getAccountAuditLogs(paymentMethod.getAccountId(), auditMode.getLevel(), tenantContext));
+ }
+
+ // Lookup the associated account(s)
+ if (accounts.get(paymentMethod.getAccountId()) == null) {
+ final Account account;
+ try {
+ account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
+ accounts.put(paymentMethod.getAccountId(), account);
+ } catch (final AccountApiException e) {
+ log.warn("Unable to retrieve account", e);
+ return null;
+ }
+ }
+
+ return PaymentMethodJson.toPaymentMethodJson(accounts.get(paymentMethod.getAccountId()), paymentMethod, accountsAuditLogs.get().get(paymentMethod.getAccountId()));
+ }
+ },
+ nextPageUri);
+ }
+
+ @GET
+ @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response searchPaymentMethods(@PathParam("searchKey") final String searchKey,
+ @QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_PAYMENT_METHOD_PLUGIN_NAME) final String pluginName,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ // Search the plugin(s)
+ final Pagination<PaymentMethod> paymentMethods;
+ if (Strings.isNullOrEmpty(pluginName)) {
+ paymentMethods = paymentApi.searchPaymentMethods(searchKey, offset, limit, tenantContext);
+ } else {
+ paymentMethods = paymentApi.searchPaymentMethods(searchKey, offset, limit, pluginName, tenantContext);
+ }
+
+ final URI nextPageUri = uriBuilder.nextPage(PaymentMethodResource.class, "searchPaymentMethods", paymentMethods.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+ QUERY_PAYMENT_METHOD_PLUGIN_NAME, Strings.nullToEmpty(pluginName),
+ QUERY_AUDIT, auditMode.getLevel().toString()));
+
+ final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+ final Map<UUID, Account> accounts = new HashMap<UUID, Account>();
+ return buildStreamingPaginationResponse(paymentMethods,
+ new Function<PaymentMethod, PaymentMethodJson>() {
+ @Override
+ public PaymentMethodJson apply(final PaymentMethod paymentMethod) {
+ // Cache audit logs per account
+ if (accountsAuditLogs.get().get(paymentMethod.getAccountId()) == null) {
+ accountsAuditLogs.get().put(paymentMethod.getAccountId(), auditUserApi.getAccountAuditLogs(paymentMethod.getAccountId(), auditMode.getLevel(), tenantContext));
+ }
+
+ // Lookup the associated account(s)
+ if (accounts.get(paymentMethod.getAccountId()) == null) {
+ final Account account;
+ try {
+ account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
+ accounts.put(paymentMethod.getAccountId(), account);
+ } catch (final AccountApiException e) {
+ log.warn("Unable to retrieve account", e);
+ return null;
+ }
+ }
+
+ return PaymentMethodJson.toPaymentMethodJson(accounts.get(paymentMethod.getAccountId()), paymentMethod, accountsAuditLogs.get().get(paymentMethod.getAccountId()));
+ }
+ },
+ nextPageUri);
+ }
+
+ @DELETE
+ @Produces(APPLICATION_JSON)
+ @Path("/{paymentMethodId:" + UUID_PATTERN + "}")
+ public Response deletePaymentMethod(@PathParam("paymentMethodId") final String paymentMethodId,
+ @QueryParam(QUERY_DELETE_DEFAULT_PM_WITH_AUTO_PAY_OFF) @DefaultValue("false") final Boolean deleteDefaultPaymentMethodWithAutoPayOff,
+ @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 PaymentApiException, AccountApiException {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+ final PaymentMethod paymentMethod = paymentApi.getPaymentMethodById(UUID.fromString(paymentMethodId), false, false, callContext);
+ final Account account = accountUserApi.getAccountById(paymentMethod.getAccountId(), callContext);
+
+ paymentApi.deletedPaymentMethod(account, UUID.fromString(paymentMethodId), deleteDefaultPaymentMethodWithAutoPayOff, callContext);
+
+ return Response.status(Status.OK).build();
+ }
+
+ @Override
+ protected ObjectType getObjectType() {
+ return ObjectType.PAYMENT_METHOD;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
new file mode 100644
index 0000000..9896791
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.math.BigDecimal;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
+
+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.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoicePayment;
+import org.killbill.billing.invoice.api.InvoicePaymentApi;
+import org.killbill.billing.jaxrs.json.ChargebackJson;
+import org.killbill.billing.jaxrs.json.CustomFieldJson;
+import org.killbill.billing.jaxrs.json.InvoiceItemJson;
+import org.killbill.billing.jaxrs.json.PaymentJson;
+import org.killbill.billing.jaxrs.json.RefundJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.Refund;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Path(JaxrsResource.PAYMENTS_PATH)
+public class PaymentResource extends JaxRsResourceBase {
+
+ private static final String ID_PARAM_NAME = "paymentId";
+
+ private final PaymentApi paymentApi;
+ private final InvoicePaymentApi invoicePaymentApi;
+
+ @Inject
+ public PaymentResource(final AccountUserApi accountUserApi,
+ final PaymentApi paymentApi,
+ final InvoicePaymentApi invoicePaymentApi,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.paymentApi = paymentApi;
+ this.invoicePaymentApi = invoicePaymentApi;
+ }
+
+ @GET
+ @Path("/{paymentId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response getPayment(@PathParam(ID_PARAM_NAME) final String paymentIdString,
+ @QueryParam(QUERY_PAYMENT_WITH_REFUNDS_AND_CHARGEBACKS) @DefaultValue("false") final Boolean withRefundsAndChargebacks,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ final UUID paymentId = UUID.fromString(paymentIdString);
+ final Payment payment = paymentApi.getPayment(paymentId, false, tenantContext);
+
+ final PaymentJson paymentJson;
+ if (withRefundsAndChargebacks) {
+ final List<RefundJson> refunds = new ArrayList<RefundJson>();
+ for (final Refund refund : paymentApi.getPaymentRefunds(paymentId, tenantContext)) {
+ refunds.add(new RefundJson(refund));
+ }
+
+ final List<ChargebackJson> chargebacks = new ArrayList<ChargebackJson>();
+ for (final InvoicePayment chargeback : invoicePaymentApi.getChargebacksByPaymentId(paymentId, tenantContext)) {
+ chargebacks.add(new ChargebackJson(payment.getAccountId(), chargeback));
+ }
+
+ paymentJson = new PaymentJson(payment,
+ null, // TODO - the keys are really only used for the timeline
+ refunds,
+ chargebacks);
+ } else {
+ paymentJson = new PaymentJson(payment, null);
+ }
+
+ return Response.status(Status.OK).entity(paymentJson).build();
+ }
+
+ @GET
+ @Path("/" + PAGINATION)
+ @Produces(APPLICATION_JSON)
+ public Response getPayments(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ final Pagination<Payment> payments;
+ if (Strings.isNullOrEmpty(pluginName)) {
+ payments = paymentApi.getPayments(offset, limit, tenantContext);
+ } else {
+ payments = paymentApi.getPayments(offset, limit, pluginName, tenantContext);
+ }
+
+ final URI nextPageUri = uriBuilder.nextPage(PaymentResource.class, "getPayments", payments.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_PAYMENT_METHOD_PLUGIN_NAME, Strings.nullToEmpty(pluginName),
+ QUERY_AUDIT, auditMode.getLevel().toString()));
+ final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+
+ return buildStreamingPaginationResponse(payments,
+ new Function<Payment, PaymentJson>() {
+ @Override
+ public PaymentJson apply(final Payment payment) {
+ // Cache audit logs per account
+ if (accountsAuditLogs.get().get(payment.getAccountId()) == null) {
+ accountsAuditLogs.get().put(payment.getAccountId(), auditUserApi.getAccountAuditLogs(payment.getAccountId(), auditMode.getLevel(), tenantContext));
+ }
+ return new PaymentJson(payment, accountsAuditLogs.get().get(payment.getAccountId()).getAuditLogsForPayment(payment.getId()));
+ }
+ },
+ nextPageUri);
+ }
+
+ @GET
+ @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response searchPayments(@PathParam("searchKey") final String searchKey,
+ @QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ // Search the plugin(s)
+ final Pagination<Payment> payments;
+ if (Strings.isNullOrEmpty(pluginName)) {
+ payments = paymentApi.searchPayments(searchKey, offset, limit, tenantContext);
+ } else {
+ payments = paymentApi.searchPayments(searchKey, offset, limit, pluginName, tenantContext);
+ }
+
+ final URI nextPageUri = uriBuilder.nextPage(PaymentResource.class, "searchPayments", payments.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+ QUERY_PAYMENT_METHOD_PLUGIN_NAME, Strings.nullToEmpty(pluginName),
+ QUERY_AUDIT, auditMode.getLevel().toString()));
+ final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+
+ return buildStreamingPaginationResponse(payments,
+ new Function<Payment, PaymentJson>() {
+ @Override
+ public PaymentJson apply(final Payment payment) {
+ // Cache audit logs per account
+ if (accountsAuditLogs.get().get(payment.getAccountId()) == null) {
+ accountsAuditLogs.get().put(payment.getAccountId(), auditUserApi.getAccountAuditLogs(payment.getAccountId(), auditMode.getLevel(), tenantContext));
+ }
+ return new PaymentJson(payment, accountsAuditLogs.get().get(payment.getAccountId()).getAuditLogsForPayment(payment.getId()));
+ }
+ },
+ nextPageUri);
+ }
+
+ @PUT
+ @Path("/{paymentId:" + UUID_PATTERN + "}")
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response retryFailedPayment(@PathParam(ID_PARAM_NAME) final String paymentIdString,
+ @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 AccountApiException, PaymentApiException {
+
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+ final UUID paymentId = UUID.fromString(paymentIdString);
+ final Payment payment = paymentApi.getPayment(paymentId, false, callContext);
+ final Account account = accountUserApi.getAccountById(payment.getAccountId(), callContext);
+ final Payment newPayment = paymentApi.retryPayment(account, paymentId, callContext);
+
+ return Response.status(Status.OK).entity(new PaymentJson(newPayment, null)).build();
+ }
+
+ @GET
+ @Path("/{paymentId:" + UUID_PATTERN + "}/" + CHARGEBACKS)
+ @Produces(APPLICATION_JSON)
+ public Response getChargebacksForPayment(@PathParam("paymentId") final String paymentId,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws InvoiceApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ final List<InvoicePayment> chargebacks = invoicePaymentApi.getChargebacksByPaymentId(UUID.fromString(paymentId), tenantContext);
+ if (chargebacks.size() == 0) {
+ return Response.status(Response.Status.NO_CONTENT).build();
+ }
+
+ final UUID invoicePaymentId = chargebacks.get(0).getId();
+ final UUID accountId = invoicePaymentApi.getAccountIdFromInvoicePaymentId(invoicePaymentId, tenantContext);
+ final List<ChargebackJson> chargebacksJson = new ArrayList<ChargebackJson>();
+ for (final InvoicePayment chargeback : chargebacks) {
+ chargebacksJson.add(new ChargebackJson(accountId, chargeback));
+ }
+ return Response.status(Response.Status.OK).entity(chargebacksJson).build();
+ }
+
+ @GET
+ @Path("/{paymentId:" + UUID_PATTERN + "}/" + REFUNDS)
+ @Produces(APPLICATION_JSON)
+ public Response getRefunds(@PathParam("paymentId") final String paymentId,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+ final List<Refund> refunds = paymentApi.getPaymentRefunds(UUID.fromString(paymentId), context.createContext(request));
+ final List<RefundJson> result = new ArrayList<RefundJson>(Collections2.transform(refunds, new Function<Refund, RefundJson>() {
+ @Override
+ public RefundJson apply(final Refund input) {
+ // TODO Return adjusted items and audits
+ return new RefundJson(input, null, null);
+ }
+ }));
+
+ return Response.status(Status.OK).entity(result).build();
+ }
+
+ @POST
+ @Path("/{paymentId:" + UUID_PATTERN + "}/" + REFUNDS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createRefund(final RefundJson json,
+ @PathParam("paymentId") final String paymentId,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final UriInfo uriInfo,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+ final UUID paymentUuid = UUID.fromString(paymentId);
+ final Payment payment = paymentApi.getPayment(paymentUuid, false, callContext);
+ final Account account = accountUserApi.getAccountById(payment.getAccountId(), callContext);
+
+ final Refund result;
+ if (json.isAdjusted()) {
+ if (json.getAdjustments() != null && json.getAdjustments().size() > 0) {
+ final Map<UUID, BigDecimal> adjustments = new HashMap<UUID, BigDecimal>();
+ for (final InvoiceItemJson item : json.getAdjustments()) {
+ adjustments.put(UUID.fromString(item.getInvoiceItemId()), item.getAmount());
+ }
+ result = paymentApi.createRefundWithItemsAdjustments(account, paymentUuid, adjustments, callContext);
+ } else {
+ // Invoice adjustment
+ result = paymentApi.createRefundWithAdjustment(account, paymentUuid, json.getAmount(), callContext);
+ }
+ } else {
+ // Refund without adjustment
+ result = paymentApi.createRefund(account, paymentUuid, json.getAmount(), callContext);
+ }
+
+ return uriBuilder.buildResponse(RefundResource.class, "getRefund", result.getId(), uriInfo.getBaseUri().toString());
+ }
+
+ @GET
+ @Path("/{paymentId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Produces(APPLICATION_JSON)
+ public Response getCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) {
+ return super.getCustomFields(UUID.fromString(id), auditMode, context.createContext(request));
+ }
+
+ @POST
+ @Path("/{paymentId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ final List<CustomFieldJson> customFields,
+ @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,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws CustomFieldApiException {
+ return super.createCustomFields(UUID.fromString(id), customFields,
+ context.createContext(createdBy, reason, comment, request), uriInfo);
+ }
+
+ @DELETE
+ @Path("/{paymentId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response deleteCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_CUSTOM_FIELDS) final String customFieldList,
+ @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 CustomFieldApiException {
+ return super.deleteCustomFields(UUID.fromString(id), customFieldList,
+ context.createContext(createdBy, reason, comment, request));
+ }
+
+ @GET
+ @Path("/{paymentId:" + UUID_PATTERN + "}/" + TAGS)
+ @Produces(APPLICATION_JSON)
+ public Response getTags(@PathParam(ID_PARAM_NAME) final String paymentIdString,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @QueryParam(QUERY_TAGS_INCLUDED_DELETED) @DefaultValue("false") final Boolean includedDeleted,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException, PaymentApiException {
+ final UUID paymentId = UUID.fromString(paymentIdString);
+ final TenantContext tenantContext = context.createContext(request);
+ final Payment payment = paymentApi.getPayment(paymentId, false, tenantContext);
+ return super.getTags(payment.getAccountId(), paymentId, auditMode, includedDeleted, tenantContext);
+ }
+
+ @POST
+ @Path("/{paymentId:" + UUID_PATTERN + "}/" + TAGS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createTags(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_TAGS) final String tagList,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final UriInfo uriInfo,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagApiException {
+ return super.createTags(UUID.fromString(id), tagList, uriInfo,
+ context.createContext(createdBy, reason, comment, request));
+ }
+
+ @DELETE
+ @Path("/{paymentId:" + UUID_PATTERN + "}/" + TAGS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response deleteTags(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_TAGS) final String tagList,
+ @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 TagApiException {
+ return super.deleteTags(UUID.fromString(id), tagList,
+ context.createContext(createdBy, reason, comment, request));
+ }
+
+ @Override
+ protected ObjectType getObjectType() {
+ return ObjectType.PAYMENT;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PluginResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PluginResource.java
new file mode 100644
index 0000000..17a30f5
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PluginResource.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.HEAD;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+
+import com.google.common.io.ByteStreams;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+@Singleton
+@Path(JaxrsResource.PLUGINS_PATH + "{subResources:.*}")
+public class PluginResource extends JaxRsResourceBase {
+
+ private static final Logger log = LoggerFactory.getLogger(PluginResource.class);
+ private static final String UTF_8_STRING = "UTF-8";
+ private static final Charset UTF_8 = Charset.forName(UTF_8_STRING);
+
+ private final HttpServlet osgiServlet;
+
+ @Inject
+ public PluginResource(@Named("osgi") final HttpServlet osgiServlet,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.osgiServlet = osgiServlet;
+ }
+
+ @DELETE
+ public Response doDELETE(@javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final HttpServletResponse response,
+ @javax.ws.rs.core.Context final ServletContext servletContext,
+ @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+ return serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
+ }
+
+ @GET
+ public Response doGET(@javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final HttpServletResponse response,
+ @javax.ws.rs.core.Context final ServletContext servletContext,
+ @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+ return serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
+ }
+
+ @OPTIONS
+ public Response doOPTIONS(@javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final HttpServletResponse response,
+ @javax.ws.rs.core.Context final ServletContext servletContext,
+ @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+ return serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
+ }
+
+ @POST
+ @Consumes("application/x-www-form-urlencoded")
+ public Response doFormPOST(final MultivaluedMap<String, String> form,
+ @javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final HttpServletResponse response,
+ @javax.ws.rs.core.Context final ServletContext servletContext,
+ @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+ return serviceViaOSGIPlugin(form, request, response, servletContext, servletConfig);
+ }
+
+ @POST
+ public Response doPOST(@javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final HttpServletResponse response,
+ @javax.ws.rs.core.Context final ServletContext servletContext,
+ @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+ return serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
+ }
+
+ @PUT
+ public Response doPUT(@javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final HttpServletResponse response,
+ @javax.ws.rs.core.Context final ServletContext servletContext,
+ @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+ return serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
+ }
+
+ @HEAD
+ public Response doHEAD(@javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final HttpServletResponse response,
+ @javax.ws.rs.core.Context final ServletContext servletContext,
+ @javax.ws.rs.core.Context final ServletConfig servletConfig) throws ServletException, IOException {
+ serviceViaOSGIPlugin(request, response, servletContext, servletConfig);
+
+ // Make sure to return 204
+ return Response.noContent().build();
+ }
+
+ private Response serviceViaOSGIPlugin(final HttpServletRequest request, final HttpServletResponse response,
+ final ServletContext servletContext, final ServletConfig servletConfig) throws ServletException, IOException {
+ return serviceViaOSGIPlugin(request, request.getInputStream(), response, servletContext, servletConfig);
+ }
+
+ private Response serviceViaOSGIPlugin(final MultivaluedMap<String, String> form,
+ final HttpServletRequest request, final HttpServletResponse response,
+ final ServletContext servletContext, final ServletConfig servletConfig) throws ServletException, IOException {
+ // form will contain form parameters, if any. Even if the request contains such parameters, it may be empty
+ // if a filter (e.g. Shiro) has already consumed them (see kludge below)
+ return serviceViaOSGIPlugin(request, createInputStream(request, form), response, servletContext, servletConfig);
+ }
+
+ private Response serviceViaOSGIPlugin(final HttpServletRequest request, final InputStream inputStream, final HttpServletResponse response,
+ final ServletContext servletContext, final ServletConfig servletConfig) throws ServletException, IOException {
+ prepareOSGIRequest(request, servletContext, servletConfig);
+ osgiServlet.service(new OSGIServletRequestWrapper(request, inputStream), new OSGIServletResponseWrapper(response));
+
+ if (response.isCommitted()) {
+ if (response.getStatus() >= 400) {
+ log.warn("{} responded {}", request.getPathInfo(), response.getStatus());
+ }
+ // Jersey will want to return 204, but the servlet should have done the right thing already
+ return null;
+ } else {
+ return Response.status(response.getStatus()).build();
+ }
+ }
+
+ private InputStream createInputStream(final HttpServletRequest request, final MultivaluedMap<String, String> form) throws IOException {
+ // /!\ Kludge alert (pierre) /!\
+ // This is awful... But because of various servlet filters we have in place, include Shiro,
+ // the request parameters and/or body at this point have already been looked at.
+ // We can't use @FormParam in PluginResource because we don't know the form parameter names
+ // in advance.
+ // So... We just stick them back in :-)
+ // TODO Support application/x-www-form-urlencoded vs multipart/form-data
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ final Map<String, String> data = new HashMap<String, String>();
+ for (final String key : request.getParameterMap().keySet()) {
+ data.put(key, request.getParameter(key));
+ }
+ for (final String key : form.keySet()) {
+ data.put(key, form.getFirst(key));
+ }
+ appendFormParametersToBody(out, data);
+
+ ByteStreams.copy(request.getInputStream(), out);
+ return new ByteArrayInputStream(out.toByteArray());
+ }
+
+ private void appendFormParametersToBody(final ByteArrayOutputStream out, final Map<String, String> data) throws IOException {
+ int idx = 0;
+ for (final String key : data.keySet()) {
+ if (idx > 0) {
+ out.write("&".getBytes(UTF_8));
+ }
+
+ out.write((key + "=" + URLEncoder.encode(data.get(key), UTF_8_STRING)).getBytes(UTF_8));
+ idx++;
+ }
+ }
+
+ private void prepareOSGIRequest(final HttpServletRequest request, final ServletContext servletContext, final ServletConfig servletConfig) {
+ request.setAttribute("killbill.osgi.servletContext", servletContext);
+ request.setAttribute("killbill.osgi.servletConfig", servletConfig);
+ }
+
+ // Request wrapper to hide the /plugins prefix to OSGI bundles and fiddle with the input stream
+ private static final class OSGIServletRequestWrapper extends HttpServletRequestWrapper {
+
+ private final InputStream inputStream;
+
+ public OSGIServletRequestWrapper(final HttpServletRequest request, final InputStream inputStream) {
+ super(request);
+ this.inputStream = inputStream;
+ }
+
+ @Override
+ public String getPathInfo() {
+ return super.getPathInfo().replace(JaxrsResource.PLUGINS_PATH, "");
+ }
+
+ @Override
+ public String getContextPath() {
+ return JaxrsResource.PLUGINS_PATH;
+ }
+
+ @Override
+ public String getServletPath() {
+ return super.getServletPath().replace(JaxrsResource.PLUGINS_PATH, "");
+ }
+
+ @Override
+ public ServletInputStream getInputStream() throws IOException {
+ return new ServletInputStreamWrapper(inputStream);
+ }
+ }
+
+ private static final class ServletInputStreamWrapper extends ServletInputStream {
+
+ private final InputStream inputStream;
+
+ public ServletInputStreamWrapper(final InputStream inputStream) {
+ this.inputStream = inputStream;
+ }
+
+ @Override
+ public int read() throws IOException {
+ return inputStream.read();
+ }
+ }
+
+ private static final class OSGIServletResponseWrapper extends HttpServletResponseWrapper {
+
+ public OSGIServletResponseWrapper(final HttpServletResponse response) {
+ super(response);
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/RefundResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/RefundResource.java
new file mode 100644
index 0000000..9c807e0
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/RefundResource.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.json.RefundJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.Refund;
+import org.killbill.billing.util.api.AuditLevel;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Path(JaxrsResource.REFUNDS_PATH)
+public class RefundResource extends JaxRsResourceBase {
+
+ private final PaymentApi paymentApi;
+
+ @Inject
+ public RefundResource(final PaymentApi paymentApi,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.paymentApi = paymentApi;
+ }
+
+ @GET
+ @Path("/{refundId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response getRefund(@PathParam("refundId") final String refundId,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final Refund refund = paymentApi.getRefund(UUID.fromString(refundId), false, tenantContext);
+ final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(refund.getId(), ObjectType.REFUND, auditMode.getLevel(), tenantContext);
+ // TODO Return adjusted items
+ return Response.status(Status.OK).entity(new RefundJson(refund, null, auditLogs)).build();
+ }
+
+ @GET
+ @Path("/" + PAGINATION)
+ @Produces(APPLICATION_JSON)
+ public Response getRefunds(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ final Pagination<Refund> refunds;
+ if (Strings.isNullOrEmpty(pluginName)) {
+ refunds = paymentApi.getRefunds(offset, limit, tenantContext);
+ } else {
+ refunds = paymentApi.getRefunds(offset, limit, pluginName, tenantContext);
+ }
+
+ final URI nextPageUri = uriBuilder.nextPage(RefundResource.class, "getRefunds", refunds.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_PAYMENT_METHOD_PLUGIN_NAME, Strings.nullToEmpty(pluginName),
+ QUERY_AUDIT, auditMode.getLevel().toString()));
+
+ final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+ final Map<UUID, UUID> paymentIdAccountIdMappings = new HashMap<UUID, UUID>();
+ return buildStreamingPaginationResponse(refunds,
+ new Function<Refund, RefundJson>() {
+ @Override
+ public RefundJson apply(final Refund refund) {
+ UUID kbAccountId = null;
+ if (!AuditLevel.NONE.equals(auditMode.getLevel()) && paymentIdAccountIdMappings.get(refund.getPaymentId()) == null) {
+ try {
+ kbAccountId = paymentApi.getPayment(refund.getPaymentId(), false, tenantContext).getAccountId();
+ paymentIdAccountIdMappings.put(refund.getPaymentId(), kbAccountId);
+ } catch (final PaymentApiException e) {
+ log.warn("Unable to retrieve payment for id " + refund.getPaymentId());
+ }
+ }
+
+ // Cache audit logs per account
+ if (accountsAuditLogs.get().get(kbAccountId) == null) {
+ accountsAuditLogs.get().put(kbAccountId, auditUserApi.getAccountAuditLogs(kbAccountId, auditMode.getLevel(), tenantContext));
+ }
+
+ final List<AuditLog> auditLogs = accountsAuditLogs.get().get(kbAccountId) == null ? null : accountsAuditLogs.get().get(kbAccountId).getAuditLogsForRefund(refund.getId());
+ return new RefundJson(refund, null, auditLogs);
+ }
+ },
+ nextPageUri);
+ }
+
+ @GET
+ @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response searchRefunds(@PathParam("searchKey") final String searchKey,
+ @QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ // Search the plugin(s)
+ final Pagination<Refund> refunds;
+ if (Strings.isNullOrEmpty(pluginName)) {
+ refunds = paymentApi.searchRefunds(searchKey, offset, limit, tenantContext);
+ } else {
+ refunds = paymentApi.searchRefunds(searchKey, offset, limit, pluginName, tenantContext);
+ }
+
+ final URI nextPageUri = uriBuilder.nextPage(RefundResource.class, "searchRefunds", refunds.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+ QUERY_PAYMENT_METHOD_PLUGIN_NAME, Strings.nullToEmpty(pluginName),
+ QUERY_AUDIT, auditMode.getLevel().toString()));
+
+ final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+ final Map<UUID, UUID> paymentIdAccountIdMappings = new HashMap<UUID, UUID>();
+ return buildStreamingPaginationResponse(refunds,
+ new Function<Refund, RefundJson>() {
+ @Override
+ public RefundJson apply(final Refund refund) {
+ UUID kbAccountId = null;
+ if (!AuditLevel.NONE.equals(auditMode.getLevel()) && paymentIdAccountIdMappings.get(refund.getPaymentId()) == null) {
+ try {
+ kbAccountId = paymentApi.getPayment(refund.getPaymentId(), false, tenantContext).getAccountId();
+ paymentIdAccountIdMappings.put(refund.getPaymentId(), kbAccountId);
+ } catch (final PaymentApiException e) {
+ log.warn("Unable to retrieve payment for id " + refund.getPaymentId());
+ }
+ }
+
+ // Cache audit logs per account
+ if (accountsAuditLogs.get().get(kbAccountId) == null) {
+ accountsAuditLogs.get().put(kbAccountId, auditUserApi.getAccountAuditLogs(kbAccountId, auditMode.getLevel(), tenantContext));
+ }
+
+ final List<AuditLog> auditLogs = accountsAuditLogs.get().get(kbAccountId) == null ? null : accountsAuditLogs.get().get(kbAccountId).getAuditLogsForRefund(refund.getId());
+ return new RefundJson(refund, null, auditLogs);
+ }
+ },
+ nextPageUri);
+ }
+
+ @Override
+ protected ObjectType getObjectType() {
+ return ObjectType.REFUND;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SecurityResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SecurityResource.java
new file mode 100644
index 0000000..94e561f
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SecurityResource.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.inject.Singleton;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.subject.Subject;
+
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.json.SubjectJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.security.Permission;
+import org.killbill.billing.security.api.SecurityApi;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.SECURITY_PATH)
+public class SecurityResource extends JaxRsResourceBase {
+
+ private final SecurityApi securityApi;
+
+ @Inject
+ public SecurityResource(final SecurityApi securityApi,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.securityApi = securityApi;
+ }
+
+ @GET
+ @Path("/permissions")
+ @Produces(APPLICATION_JSON)
+ public Response getCurrentUserPermissions(@javax.ws.rs.core.Context final HttpServletRequest request) {
+ final Set<Permission> permissions = securityApi.getCurrentUserPermissions(context.createContext(request));
+ final List<String> json = ImmutableList.<String>copyOf(Iterables.<Permission, String>transform(permissions, Functions.toStringFunction()));
+ return Response.status(Status.OK).entity(json).build();
+ }
+
+ @GET
+ @Path("/subject")
+ @Produces(APPLICATION_JSON)
+ public Response getCurrentUserSubject(@javax.ws.rs.core.Context final HttpServletRequest request) {
+ final Subject subject = SecurityUtils.getSubject();
+ final SubjectJson subjectJson = new SubjectJson(subject);
+ return Response.status(Status.OK).entity(subjectJson).build();
+ }
+}
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
new file mode 100644
index 0000000..deee259
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -0,0 +1,477 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeoutException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
+
+import org.joda.time.LocalDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
+import org.killbill.billing.entitlement.api.EntitlementApi;
+import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.entitlement.api.Subscription;
+import org.killbill.billing.entitlement.api.SubscriptionApi;
+import org.killbill.billing.entitlement.api.SubscriptionApiException;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.events.InvoiceCreationInternalEvent;
+import org.killbill.billing.events.NullInvoiceInternalEvent;
+import org.killbill.billing.events.PaymentErrorInternalEvent;
+import org.killbill.billing.events.PaymentInfoInternalEvent;
+import org.killbill.billing.events.PaymentPluginErrorInternalEvent;
+import org.killbill.billing.jaxrs.json.CustomFieldJson;
+import org.killbill.billing.jaxrs.json.SubscriptionJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.jaxrs.util.KillbillEventHandler;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.userrequest.CompletionUserRequestBase;
+
+import com.google.inject.Inject;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Path(JaxrsResource.SUBSCRIPTIONS_PATH)
+public class SubscriptionResource extends JaxRsResourceBase {
+
+ private static final Logger log = LoggerFactory.getLogger(SubscriptionResource.class);
+ private static final String ID_PARAM_NAME = "subscriptionId";
+
+ private final KillbillEventHandler killbillHandler;
+ private final EntitlementApi entitlementApi;
+ private final SubscriptionApi subscriptionApi;
+
+ @Inject
+ public SubscriptionResource(final KillbillEventHandler killbillHandler,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final EntitlementApi entitlementApi,
+ final SubscriptionApi subscriptionApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.killbillHandler = killbillHandler;
+ this.entitlementApi = entitlementApi;
+ this.subscriptionApi = subscriptionApi;
+ }
+
+ @GET
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response getEntitlement(@PathParam("subscriptionId") final String subscriptionId,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
+ final UUID uuid = UUID.fromString(subscriptionId);
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(uuid, context.createContext(request));
+ final SubscriptionJson json = new SubscriptionJson(subscription, null, null);
+ return Response.status(Status.OK).entity(json).build();
+ }
+
+ @POST
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createEntitlement(final SubscriptionJson entitlement,
+ @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
+ @QueryParam(QUERY_CALL_COMPLETION) @DefaultValue("false") final Boolean callCompletion,
+ @QueryParam(QUERY_CALL_TIMEOUT) @DefaultValue("3") final long timeoutSec,
+ @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,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws EntitlementApiException, AccountApiException, SubscriptionApiException {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+ final EntitlementCallCompletionCallback<Entitlement> callback = new EntitlementCallCompletionCallback<Entitlement>() {
+ @Override
+ public Entitlement doOperation(final CallContext ctx) throws InterruptedException, TimeoutException, EntitlementApiException {
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(entitlement.getProductName(),
+ ProductCategory.valueOf(entitlement.getProductCategory()),
+ BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), null);
+
+ final UUID accountId = entitlement.getAccountId() != null ? UUID.fromString(entitlement.getAccountId()) : null;
+ final LocalDate inputLocalDate = toLocalDate(accountId, requestedDate, callContext);
+ final UUID bundleId = entitlement.getBundleId() != null ? UUID.fromString(entitlement.getBundleId()) : null;
+ return (entitlement.getProductCategory().equals(ProductCategory.ADD_ON.toString())) ?
+ entitlementApi.addEntitlement(bundleId, spec, inputLocalDate, callContext) :
+ entitlementApi.createBaseEntitlement(accountId, spec, entitlement.getExternalKey(), inputLocalDate, callContext);
+ }
+
+ @Override
+ public boolean isImmOperation() {
+ return true;
+ }
+
+ @Override
+ public Response doResponseOk(final Entitlement createdEntitlement) {
+ return uriBuilder.buildResponse(uriInfo, SubscriptionResource.class, "getEntitlement", createdEntitlement.getId());
+ }
+ };
+
+ final EntitlementCallCompletion<Entitlement> callCompletionCreation = new EntitlementCallCompletion<Entitlement>();
+ return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
+ }
+
+ @PUT
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}/uncancel")
+ @Produces(APPLICATION_JSON)
+ public Response uncancelEntitlementPlan(@PathParam("subscriptionId") final String subscriptionId,
+ @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 EntitlementApiException {
+ final UUID uuid = UUID.fromString(subscriptionId);
+ final Entitlement current = entitlementApi.getEntitlementForId(uuid, context.createContext(createdBy, reason, comment, request));
+ current.uncancelEntitlement(context.createContext(createdBy, reason, comment, request));
+ return Response.status(Status.OK).build();
+ }
+
+ @PUT
+ @Produces(APPLICATION_JSON)
+ @Consumes(APPLICATION_JSON)
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}")
+ public Response changeEntitlementPlan(final SubscriptionJson entitlement,
+ @PathParam("subscriptionId") final String subscriptionId,
+ @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
+ @QueryParam(QUERY_CALL_COMPLETION) @DefaultValue("false") final Boolean callCompletion,
+ @QueryParam(QUERY_CALL_TIMEOUT) @DefaultValue("3") final long timeoutSec,
+ @QueryParam(QUERY_BILLING_POLICY) final String policyString,
+ @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 EntitlementApiException, AccountApiException, SubscriptionApiException {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+ final EntitlementCallCompletionCallback<Response> callback = new EntitlementCallCompletionCallback<Response>() {
+
+ private boolean isImmediateOp = true;
+
+ @Override
+ public Response doOperation(final CallContext ctx) throws EntitlementApiException, InterruptedException,
+ TimeoutException, AccountApiException {
+ final UUID uuid = UUID.fromString(subscriptionId);
+
+ final Entitlement current = entitlementApi.getEntitlementForId(uuid, callContext);
+ final LocalDate inputLocalDate = toLocalDate(current.getAccountId(), requestedDate, callContext);
+ final Entitlement newEntitlement;
+ if (requestedDate == null && policyString == null) {
+ newEntitlement = current.changePlan(entitlement.getProductName(), BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), ctx);
+ } else if (policyString == null) {
+ newEntitlement = current.changePlanWithDate(entitlement.getProductName(), BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), inputLocalDate, ctx);
+ } else {
+ final BillingActionPolicy policy = BillingActionPolicy.valueOf(policyString.toUpperCase());
+ newEntitlement = current.changePlanOverrideBillingPolicy(entitlement.getProductName(), BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), inputLocalDate, policy, ctx);
+ }
+ isImmediateOp = newEntitlement.getLastActiveProduct().getName().equals(entitlement.getProductName()) &&
+ newEntitlement.getLastActivePlan().getBillingPeriod() == BillingPeriod.valueOf(entitlement.getBillingPeriod()) &&
+ newEntitlement.getLastActivePriceList().getName().equals(entitlement.getPriceList());
+ return Response.status(Status.OK).build();
+ }
+
+ @Override
+ public boolean isImmOperation() {
+ return isImmediateOp;
+ }
+
+ @Override
+ public Response doResponseOk(final Response operationResponse) throws SubscriptionApiException {
+ if (operationResponse.getStatus() != Status.OK.getStatusCode()) {
+ return operationResponse;
+ }
+ return getEntitlement(subscriptionId, request);
+ }
+ };
+
+ final EntitlementCallCompletion<Response> callCompletionCreation = new EntitlementCallCompletion<Response>();
+ return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
+ }
+
+ @DELETE
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response cancelEntitlementPlan(@PathParam("subscriptionId") final String subscriptionId,
+ @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
+ @QueryParam(QUERY_CALL_COMPLETION) @DefaultValue("false") final Boolean callCompletion,
+ @QueryParam(QUERY_CALL_TIMEOUT) @DefaultValue("5") final long timeoutSec,
+ @QueryParam(QUERY_ENTITLEMENT_POLICY) final String entitlementPolicyString,
+ @QueryParam(QUERY_BILLING_POLICY) final String billingPolicyString,
+ @QueryParam(QUERY_USE_REQUESTED_DATE_FOR_BILLING) @DefaultValue("false") final Boolean useRequestedDateForBilling,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final UriInfo uriInfo,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws EntitlementApiException, AccountApiException, SubscriptionApiException {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+ final EntitlementCallCompletionCallback<Response> callback = new EntitlementCallCompletionCallback<Response>() {
+
+ private boolean isImmediateOp = true;
+
+ @Override
+ public Response doOperation(final CallContext ctx)
+ throws EntitlementApiException, InterruptedException,
+ TimeoutException, AccountApiException, SubscriptionApiException {
+ final UUID uuid = UUID.fromString(subscriptionId);
+
+ final Entitlement current = entitlementApi.getEntitlementForId(uuid, ctx);
+
+ final LocalDate inputLocalDate = toLocalDate(current.getAccountId(), requestedDate, callContext);
+ final Entitlement newEntitlement;
+ if (billingPolicyString == null && entitlementPolicyString == null) {
+ newEntitlement = current.cancelEntitlementWithDate(inputLocalDate, useRequestedDateForBilling, ctx);
+ } else if (billingPolicyString == null && entitlementPolicyString != null) {
+ final EntitlementActionPolicy entitlementPolicy = EntitlementActionPolicy.valueOf(entitlementPolicyString);
+ newEntitlement = current.cancelEntitlementWithPolicy(entitlementPolicy, ctx);
+ } else if (billingPolicyString != null && entitlementPolicyString == null) {
+ final BillingActionPolicy billingPolicy = BillingActionPolicy.valueOf(billingPolicyString.toUpperCase());
+ newEntitlement = current.cancelEntitlementWithDateOverrideBillingPolicy(inputLocalDate, billingPolicy, ctx);
+ } else {
+ final EntitlementActionPolicy entitlementPolicy = EntitlementActionPolicy.valueOf(entitlementPolicyString);
+ final BillingActionPolicy billingPolicy = BillingActionPolicy.valueOf(billingPolicyString.toUpperCase());
+ newEntitlement = current.cancelEntitlementWithPolicyOverrideBillingPolicy(entitlementPolicy, billingPolicy, ctx);
+ }
+
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(newEntitlement.getId(), ctx);
+
+ final LocalDate nowInAccountTimeZone = new LocalDate(clock.getUTCNow(), subscription.getBillingEndDate().getChronology().getZone());
+ isImmediateOp = subscription.getBillingEndDate() != null &&
+ !subscription.getBillingEndDate().isAfter(nowInAccountTimeZone);
+ return Response.status(Status.OK).build();
+ }
+
+ @Override
+ public boolean isImmOperation() {
+ return isImmediateOp;
+ }
+
+ @Override
+ public Response doResponseOk(final Response operationResponse) {
+ return operationResponse;
+ }
+ };
+
+ final EntitlementCallCompletion<Response> callCompletionCreation = new EntitlementCallCompletion<Response>();
+ return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
+ }
+
+ private static final class CompletionUserRequestEntitlement extends CompletionUserRequestBase {
+
+ public CompletionUserRequestEntitlement(final UUID userToken) {
+ super(userToken);
+ }
+
+ @Override
+ public void onSubscriptionBaseTransition(final EffectiveSubscriptionInternalEvent event) {
+
+ log.info(String.format("Got event SubscriptionBaseTransition token = %s, type = %s, remaining = %d ",
+ event.getUserToken(), event.getTransitionType(), event.getRemainingEventsForUserOperation()));
+ }
+
+ @Override
+ public void onEmptyInvoice(final NullInvoiceInternalEvent event) {
+ log.info(String.format("Got event EmptyInvoiceNotification token = %s ", event.getUserToken()));
+ notifyForCompletion();
+ }
+
+ @Override
+ public void onInvoiceCreation(final InvoiceCreationInternalEvent event) {
+
+ log.info(String.format("Got event InvoiceCreationNotification token = %s ", event.getUserToken()));
+ if (event.getAmountOwed().compareTo(BigDecimal.ZERO) <= 0) {
+ notifyForCompletion();
+ }
+ }
+
+ @Override
+ public void onPaymentInfo(final PaymentInfoInternalEvent event) {
+ log.info(String.format("Got event PaymentInfo token = %s ", event.getUserToken()));
+ notifyForCompletion();
+ }
+
+ @Override
+ public void onPaymentError(final PaymentErrorInternalEvent event) {
+ log.info(String.format("Got event PaymentError token = %s ", event.getUserToken()));
+ notifyForCompletion();
+ }
+
+ @Override
+ public void onPaymentPluginError(final PaymentPluginErrorInternalEvent event) {
+ log.info(String.format("Got event PaymentPluginError token = %s ", event.getUserToken()));
+ notifyForCompletion();
+ }
+ }
+
+ private interface EntitlementCallCompletionCallback<T> {
+
+ public T doOperation(final CallContext ctx) throws EntitlementApiException, InterruptedException, TimeoutException, AccountApiException, SubscriptionApiException;
+
+ public boolean isImmOperation();
+
+ public Response doResponseOk(final T operationResponse) throws SubscriptionApiException;
+ }
+
+ private class EntitlementCallCompletion<T> {
+
+ public Response withSynchronization(final EntitlementCallCompletionCallback<T> callback,
+ final long timeoutSec,
+ final boolean callCompletion,
+ final CallContext callContext) throws SubscriptionApiException, AccountApiException, EntitlementApiException {
+ final CompletionUserRequestEntitlement waiter = callCompletion ? new CompletionUserRequestEntitlement(callContext.getUserToken()) : null;
+ try {
+ if (waiter != null) {
+ killbillHandler.registerCompletionUserRequestWaiter(waiter);
+ }
+ final T operationValue = callback.doOperation(callContext);
+ if (waiter != null && callback.isImmOperation()) {
+ waiter.waitForCompletion(timeoutSec * 1000);
+ }
+ return callback.doResponseOk(operationValue);
+ } catch (InterruptedException e) {
+ return Response.status(Status.INTERNAL_SERVER_ERROR).build();
+ } catch (TimeoutException e) {
+ return Response.status(Status.fromStatusCode(408)).build();
+ } finally {
+ if (waiter != null) {
+ killbillHandler.unregisterCompletionUserRequestWaiter(waiter);
+ }
+ }
+ }
+ }
+
+ @GET
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Produces(APPLICATION_JSON)
+ public Response getCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) {
+ return super.getCustomFields(UUID.fromString(id), auditMode, context.createContext(request));
+ }
+
+ @POST
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ final List<CustomFieldJson> customFields,
+ @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,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws CustomFieldApiException {
+ return super.createCustomFields(UUID.fromString(id), customFields,
+ context.createContext(createdBy, reason, comment, request), uriInfo);
+ }
+
+ @DELETE
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response deleteCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_CUSTOM_FIELDS) final String customFieldList,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final UriInfo uriInfo,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws CustomFieldApiException {
+ return super.deleteCustomFields(UUID.fromString(id), customFieldList,
+ context.createContext(createdBy, reason, comment, request));
+ }
+
+ @GET
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}/" + TAGS)
+ @Produces(APPLICATION_JSON)
+ public Response getTags(@PathParam(ID_PARAM_NAME) final String subscriptionIdString,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @QueryParam(QUERY_TAGS_INCLUDED_DELETED) @DefaultValue("false") final Boolean includedDeleted,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException, SubscriptionApiException {
+ final UUID subscriptionId = UUID.fromString(subscriptionIdString);
+ final TenantContext tenantContext = context.createContext(request);
+ final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(subscriptionId, tenantContext);
+ return super.getTags(subscription.getAccountId(), subscriptionId, auditMode, includedDeleted, tenantContext);
+ }
+
+ @POST
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}/" + TAGS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createTags(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_TAGS) final String tagList,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final UriInfo uriInfo,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagApiException {
+ return super.createTags(UUID.fromString(id), tagList, uriInfo,
+ context.createContext(createdBy, reason, comment, request));
+ }
+
+ @DELETE
+ @Path("/{subscriptionId:" + UUID_PATTERN + "}/" + TAGS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response deleteTags(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_TAGS) final String tagList,
+ @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 TagApiException {
+ return super.deleteTags(UUID.fromString(id), tagList,
+ context.createContext(createdBy, reason, comment, request));
+ }
+
+ @Override
+ protected ObjectType getObjectType() {
+ return ObjectType.SUBSCRIPTION;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TagDefinitionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TagDefinitionResource.java
new file mode 100644
index 0000000..4c51f68
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TagDefinitionResource.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.json.TagDefinitionJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.TAG_DEFINITIONS_PATH)
+public class TagDefinitionResource extends JaxRsResourceBase {
+
+ @Inject
+ public TagDefinitionResource(final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ }
+
+ @GET
+ @Produces(APPLICATION_JSON)
+ public Response getTagDefinitions(@javax.ws.rs.core.Context final HttpServletRequest request,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode) {
+ final TenantContext tenantContext = context.createContext(request);
+ final List<TagDefinition> tagDefinitions = tagUserApi.getTagDefinitions(tenantContext);
+
+ final Collection<TagDefinitionJson> result = new LinkedList<TagDefinitionJson>();
+ for (final TagDefinition tagDefinition : tagDefinitions) {
+ final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(tagDefinition.getId(), ObjectType.TAG_DEFINITION, auditMode.getLevel(), tenantContext);
+ result.add(new TagDefinitionJson(tagDefinition, auditLogs));
+ }
+
+ return Response.status(Status.OK).entity(result).build();
+ }
+
+ @GET
+ @Path("/{tagDefinitionId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response getTagDefinition(@PathParam("tagDefinitionId") final String tagDefId,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final TagDefinition tagDefinition = tagUserApi.getTagDefinition(UUID.fromString(tagDefId), tenantContext);
+ final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(tagDefinition.getId(), ObjectType.TAG_DEFINITION, auditMode.getLevel(), tenantContext);
+ final TagDefinitionJson json = new TagDefinitionJson(tagDefinition, auditLogs);
+ return Response.status(Status.OK).entity(json).build();
+ }
+
+ @POST
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createTagDefinition(final TagDefinitionJson json,
+ @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,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws TagDefinitionApiException {
+ // Checked as the database layer as well, but bail early and return 400 instead of 500
+ Preconditions.checkNotNull(json.getName(), String.format("TagDefinition name needs to be set"));
+ Preconditions.checkNotNull(json.getDescription(), String.format("TagDefinition description needs to be set"));
+
+ final TagDefinition createdTagDef = tagUserApi.createTagDefinition(json.getName(), json.getDescription(), context.createContext(createdBy, reason, comment, request));
+ return uriBuilder.buildResponse(uriInfo, TagDefinitionResource.class, "getTagDefinition", createdTagDef.getId());
+ }
+
+ @DELETE
+ @Path("/{tagDefinitionId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response deleteTagDefinition(@PathParam("tagDefinitionId") final String tagDefId,
+ @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 TagDefinitionApiException {
+ tagUserApi.deleteTagDefinition(UUID.fromString(tagDefId), context.createContext(createdBy, reason, comment, request));
+ return Response.status(Status.NO_CONTENT).build();
+ }
+
+ @Override
+ protected ObjectType getObjectType() {
+ return ObjectType.TAG_DEFINITION;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TagResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TagResource.java
new file mode 100644
index 0000000..b07d467
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TagResource.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.json.TagJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.tag.Tag;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.TAGS_PATH)
+public class TagResource extends JaxRsResourceBase {
+
+ @Inject
+ public TagResource(final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ }
+
+ @GET
+ @Path("/" + PAGINATION)
+ @Produces(APPLICATION_JSON)
+ public Response getTags(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final Pagination<Tag> tags = tagUserApi.getTags(offset, limit, tenantContext);
+ final URI nextPageUri = uriBuilder.nextPage(TagResource.class, "getTags", tags.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_AUDIT, auditMode.getLevel().toString()));
+
+ final Map<UUID, TagDefinition> tagDefinitionsCache = new HashMap<UUID, TagDefinition>();
+ for (final TagDefinition tagDefinition : tagUserApi.getTagDefinitions(tenantContext)) {
+ tagDefinitionsCache.put(tagDefinition.getId(), tagDefinition);
+ }
+
+ return buildStreamingPaginationResponse(tags,
+ new Function<Tag, TagJson>() {
+ @Override
+ public TagJson apply(final Tag tag) {
+ final TagDefinition tagDefinition = tagDefinitionsCache.get(tag.getTagDefinitionId());
+
+ // TODO Really slow - we should instead try to figure out the account id
+ final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(tag.getId(), ObjectType.TAG, auditMode.getLevel(), tenantContext);
+ return new TagJson(tag, tagDefinition, auditLogs);
+ }
+ },
+ nextPageUri);
+ }
+
+ @GET
+ @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response searchTags(@PathParam("searchKey") final String searchKey,
+ @QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagApiException {
+ final TenantContext tenantContext = context.createContext(request);
+ final Pagination<Tag> tags = tagUserApi.searchTags(searchKey, offset, limit, tenantContext);
+ final URI nextPageUri = uriBuilder.nextPage(TagResource.class, "searchTags", tags.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+ QUERY_AUDIT, auditMode.getLevel().toString()));
+ final Map<UUID, TagDefinition> tagDefinitionsCache = new HashMap<UUID, TagDefinition>();
+ for (final TagDefinition tagDefinition : tagUserApi.getTagDefinitions(tenantContext)) {
+ tagDefinitionsCache.put(tagDefinition.getId(), tagDefinition);
+ }
+ return buildStreamingPaginationResponse(tags,
+ new Function<Tag, TagJson>() {
+ @Override
+ public TagJson apply(final Tag tag) {
+ final TagDefinition tagDefinition = tagDefinitionsCache.get(tag.getTagDefinitionId());
+
+ // TODO Really slow - we should instead try to figure out the account id
+ final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(tag.getId(), ObjectType.TAG, auditMode.getLevel(), tenantContext);
+ return new TagJson(tag, tagDefinition, auditLogs);
+ }
+ },
+ nextPageUri);
+ }
+}
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
new file mode 100644
index 0000000..e20cfce
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TenantResource.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.net.URI;
+import java.util.List;
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.billing.jaxrs.json.TenantJson;
+import org.killbill.billing.jaxrs.json.TenantKeyJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.tenant.api.TenantApiException;
+import org.killbill.billing.tenant.api.TenantData;
+import org.killbill.billing.tenant.api.TenantKV.TenantKey;
+import org.killbill.billing.tenant.api.TenantUserApi;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.TENANTS_PATH)
+public class TenantResource extends JaxRsResourceBase {
+
+ private final TenantUserApi tenantApi;
+
+ @Inject
+ public TenantResource(final TenantUserApi tenantApi,
+ final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi,
+ final AccountUserApi accountUserApi,
+ final Clock clock,
+ final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.tenantApi = tenantApi;
+ }
+
+ @GET
+ @Path("/{tenantId:" + UUID_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response getTenant(@PathParam("tenantId") final String tenantId) throws TenantApiException {
+ final Tenant tenant = tenantApi.getTenantById(UUID.fromString(tenantId));
+ return Response.status(Status.OK).entity(new TenantJson(tenant)).build();
+ }
+
+ @GET
+ @Produces(APPLICATION_JSON)
+ public Response getTenantByApiKey(@QueryParam(QUERY_API_KEY) final String externalKey) throws TenantApiException {
+ final Tenant tenant = tenantApi.getTenantByApiKey(externalKey);
+ return Response.status(Status.OK).entity(new TenantJson(tenant)).build();
+ }
+
+ @POST
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response createTenant(final TenantJson json,
+ @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,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws TenantApiException {
+ final TenantData data = json.toTenantData();
+ final Tenant tenant = tenantApi.createTenant(data, context.createContext(createdBy, reason, comment, request));
+ return uriBuilder.buildResponse(uriInfo, TenantResource.class, "getTenant", tenant.getId());
+ }
+
+ @POST
+ @Path("/" + REGISTER_NOTIFICATION_CALLBACK)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response registerPushNotificationCallback(@PathParam("tenantId") final String tenantId,
+ @QueryParam(QUERY_NOTIFICATION_CALLBACK) final String notificationCallback,
+ @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 TenantApiException {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+ tenantApi.addTenantKeyValue(TenantKey.PUSH_NOTIFICATION_CB.toString(), notificationCallback, callContext);
+ final URI uri = UriBuilder.fromResource(TenantResource.class).path(TenantResource.class, "getPushNotificationCallbacks").build();
+ return Response.created(uri).build();
+ }
+
+ @GET
+ @Path("/" + REGISTER_NOTIFICATION_CALLBACK)
+ @Produces(APPLICATION_JSON)
+ public Response getPushNotificationCallbacks(@javax.ws.rs.core.Context final HttpServletRequest request) throws TenantApiException {
+
+ final TenantContext tenatContext = context.createContext(request);
+ final List<String> values = tenantApi.getTenantValueForKey(TenantKey.PUSH_NOTIFICATION_CB.toString(), tenatContext);
+ final TenantKeyJson result = new TenantKeyJson(TenantKey.PUSH_NOTIFICATION_CB.toString(), values);
+ return Response.status(Status.OK).entity(result).build();
+ }
+
+ @DELETE
+ @Path("/REGISTER_NOTIFICATION_CALLBACK")
+ public Response deletePushNotificationCallbacks(@PathParam("tenantId") final String tenantId,
+ @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 TenantApiException {
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+ tenantApi.deleteTenantKey(TenantKey.PUSH_NOTIFICATION_CB.toString(), callContext);
+ return Response.status(Status.OK).build();
+ }
+
+ @Override
+ 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
new file mode 100644
index 0000000..0ce90a1
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/TestResource.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.resources;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.RecordIdApi;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+//
+// Test endpoint that should not be enabled on a production system.
+// The clock manipulation will only work if the ClockMock instance was injected
+// throughout the system; if not it will throw 500 (UnsupportedOperationException)
+//
+// Note that moving the clock back and forth on a running system may cause weird side effects,
+// so to be used with great caution.
+//
+//
+@Path(JaxrsResource.PREFIX + "/test")
+public class TestResource extends JaxRsResourceBase {
+
+ private static final Logger log = LoggerFactory.getLogger(TestResource.class);
+ private static final int MILLIS_IN_SEC = 1000;
+
+ private final NotificationQueueService notificationQueueService;
+ private final RecordIdApi recordIdApi;
+
+ @Inject
+ public TestResource(final JaxrsUriBuilder uriBuilder, final TagUserApi tagUserApi, final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi, final AccountUserApi accountUserApi, final RecordIdApi recordIdApi,
+ final NotificationQueueService notificationQueueService,
+ final Clock clock, final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+ this.notificationQueueService = notificationQueueService;
+ this.recordIdApi = recordIdApi;
+ }
+
+
+ public final class ClockResource {
+
+ private final DateTime currentUtcTime;
+ private final String timeZone;
+ private final LocalDate localDate;
+
+ @JsonCreator
+ public ClockResource(@JsonProperty("currentUtcTime") final DateTime currentUtcTime,
+ @JsonProperty("timeZone") final String timeZone,
+ @JsonProperty("localDate") final LocalDate localDate) {
+
+ this.currentUtcTime = currentUtcTime;
+ this.timeZone = timeZone;
+ this.localDate = localDate;
+ }
+
+ public DateTime getCurrentUtcTime() {
+ return currentUtcTime;
+ }
+
+ public String getTimeZone() {
+ return timeZone;
+ }
+
+ public LocalDate getLocalDate() {
+ return localDate;
+ }
+ }
+
+ @GET
+ @Path("/clock")
+ @Produces(APPLICATION_JSON)
+ public Response getCurrentTime(@QueryParam("timeZone") final String timeZoneStr) {
+ final DateTimeZone timeZone = timeZoneStr != null ? DateTimeZone.forID(timeZoneStr) : DateTimeZone.UTC;
+ final DateTime now = clock.getUTCNow();
+ final ClockResource result = new ClockResource(now, timeZone.getID(), new LocalDate(now, timeZone));
+ return Response.status(Status.OK).entity(result).build();
+ }
+
+ @POST
+ @Path("/clock")
+ @Produces(APPLICATION_JSON)
+ public Response setTestClockTime(@QueryParam(QUERY_REQUESTED_DT) final String requestedClockDate,
+ @QueryParam("timeZone") final String timeZoneStr,
+ @QueryParam("timeoutSec") @DefaultValue("5") final Long timeoutSec,
+ @javax.ws.rs.core.Context final HttpServletRequest request) {
+
+ final ClockMock testClock = getClockMock();
+ if (requestedClockDate == null) {
+ log.info("************ RESETTING CLOCK to " + clock.getUTCNow());
+ testClock.resetDeltaFromReality();
+ } else {
+ final DateTime newTime = DATE_TIME_FORMATTER.parseDateTime(requestedClockDate);
+ testClock.setTime(newTime);
+ }
+
+ waitForNotificationToComplete(request, timeoutSec);
+
+ return getCurrentTime(timeZoneStr);
+ }
+
+
+ @PUT
+ @Path("/clock")
+ @Produces(APPLICATION_JSON)
+ public Response updateTestClockTime(@QueryParam("days") final Integer addDays,
+ @QueryParam("weeks") final Integer addWeeks,
+ @QueryParam("months") final Integer addMonths,
+ @QueryParam("years") final Integer addYears,
+ @QueryParam("timeZone") final String timeZoneStr,
+ @QueryParam("timeoutSec") @DefaultValue("5") final Long timeoutSec,
+ @javax.ws.rs.core.Context final HttpServletRequest request) {
+
+ final ClockMock testClock = getClockMock();
+ if (addDays != null) {
+ testClock.addDays(addDays);
+ } else if (addWeeks != null) {
+ testClock.addWeeks(addWeeks);
+ } else if (addMonths != null) {
+ testClock.addMonths(addMonths);
+ } else if (addYears != null) {
+ testClock.addYears(addYears);
+ }
+
+ waitForNotificationToComplete(request, timeoutSec);
+
+ return getCurrentTime(timeZoneStr);
+ }
+
+
+ private void waitForNotificationToComplete(final HttpServletRequest request, final Long timeoutSec) {
+
+ final TenantContext tenantContext = context.createContext(request);
+ final Long tenantRecordId = recordIdApi.getRecordId(tenantContext.getTenantId(), ObjectType.TENANT, tenantContext);
+ final List<NotificationQueue> queues = notificationQueueService.getNotificationQueues();
+
+ int nbTryLeft = timeoutSec != null ? timeoutSec.intValue() : 0;
+ try {
+ boolean areAllNotificationsProcessed = false;
+ while (!areAllNotificationsProcessed && nbTryLeft > 0) {
+ areAllNotificationsProcessed = areAllNotificationsProcessed(queues, tenantRecordId);
+ if (!areAllNotificationsProcessed) {
+ Thread.sleep(MILLIS_IN_SEC);
+ nbTryLeft--;
+ }
+ }
+ ;
+ } catch (InterruptedException ignore) {
+ }
+ }
+
+ private boolean areAllNotificationsProcessed(final List<NotificationQueue> queues, final Long tenantRecordId) {
+
+ final Iterable<NotificationQueue> filtered = Iterables.filter(queues, new Predicate<NotificationQueue>() {
+ @Override
+ public boolean apply(@Nullable final NotificationQueue input) {
+ return input.getReadyNotificationEntriesForSearchKey2(tenantRecordId) > 0;
+ }
+ });
+ return !filtered.iterator().hasNext();
+ }
+
+ private ClockMock getClockMock() {
+ if (!(clock instanceof ClockMock)) {
+ throw new UnsupportedOperationException("Kill Bill has not been configured to update the time");
+ }
+ return (ClockMock) clock;
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/Context.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/Context.java
new file mode 100644
index 0000000..1da2c3b
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/Context.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.util;
+
+import java.util.UUID;
+
+import javax.servlet.ServletRequest;
+
+import org.killbill.billing.jaxrs.resources.JaxrsResource;
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.CallContextFactory;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.callcontext.UserType;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+
+public class Context {
+
+ private final CallOrigin origin;
+ private final UserType userType;
+ final CallContextFactory contextFactory;
+
+ @Inject
+ public Context(final CallContextFactory factory) {
+ this.origin = CallOrigin.EXTERNAL;
+ this.userType = UserType.CUSTOMER;
+ this.contextFactory = factory;
+ }
+
+ public CallContext createContext(final String createdBy, final String reason, final String comment, final ServletRequest request)
+ throws IllegalArgumentException {
+ try {
+ Preconditions.checkNotNull(createdBy, String.format("Header %s needs to be set", JaxrsResource.HDR_CREATED_BY));
+ final Tenant tenant = getTenantFromRequest(request);
+ return contextFactory.createCallContext(tenant == null ? null : tenant.getId(), createdBy, origin, userType, reason,
+ comment, UUID.randomUUID());
+ } catch (NullPointerException e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+
+ public TenantContext createContext(final ServletRequest request) {
+ final Tenant tenant = getTenantFromRequest(request);
+ if (tenant == null) {
+ // Multi-tenancy may not have been configured - default to "default" tenant (see InternalCallContextFactory)
+ return contextFactory.createTenantContext(null);
+ } else {
+ return contextFactory.createTenantContext(tenant.getId());
+ }
+ }
+
+ private Tenant getTenantFromRequest(final ServletRequest request) {
+ // See org.killbill.billing.server.security.TenantFilter
+ final Object tenantObject = request.getAttribute("killbill_tenant");
+ if (tenantObject == null) {
+ return null;
+ } else {
+ return (Tenant) tenantObject;
+ }
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
new file mode 100644
index 0000000..cd1842b
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.util;
+
+import java.net.URI;
+import java.util.Map;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.killbill.billing.jaxrs.resources.JaxRsResourceBase;
+import org.killbill.billing.jaxrs.resources.JaxrsResource;
+
+public class JaxrsUriBuilder {
+
+ public Response buildResponse(final UriInfo uriInfo, final Class<? extends JaxrsResource> theClass, final String getMethodName, final Object objectId) {
+ final UriBuilder uriBuilder = UriBuilder.fromResource(theClass)
+ .path(theClass, getMethodName)
+ .scheme(uriInfo.getAbsolutePath().getScheme())
+ .host(uriInfo.getAbsolutePath().getHost())
+ .port(uriInfo.getAbsolutePath().getPort());
+
+ final URI location = objectId != null ? uriBuilder.build(objectId) : uriBuilder.build();
+
+ return Response.created(location).build();
+ }
+
+ public URI nextPage(final Class<? extends JaxrsResource> theClass, final String getMethodName, final Long nextOffset, final Long limit, final Map<String, String> params) {
+ if (nextOffset == null || limit == null) {
+ // End of pagination?
+ return null;
+ }
+
+ final UriBuilder uriBuilder = UriBuilder.fromResource(theClass)
+ .path(theClass, getMethodName)
+ .queryParam(JaxRsResourceBase.QUERY_SEARCH_OFFSET, nextOffset)
+ .queryParam(JaxRsResourceBase.QUERY_SEARCH_LIMIT, limit);
+ for (final String key : params.keySet()) {
+ uriBuilder.queryParam(key, params.get(key));
+ }
+ return uriBuilder.build();
+ }
+
+ public Response buildResponse(final Class<? extends JaxrsResource> theClass, final String getMethodName, final Object objectId, final String baseUri) {
+
+ // Let's build a n absolute location for cross resources
+ // See Jersey ContainerResponse.setHeaders
+ final StringBuilder tmp = new StringBuilder(baseUri.substring(0, baseUri.length() - 1));
+ tmp.append(UriBuilder.fromResource(theClass).path(theClass, getMethodName).build(objectId).toString());
+ final URI newUriFromResource = UriBuilder.fromUri(tmp.toString()).build();
+ final Response.ResponseBuilder ri = Response.created(newUriFromResource);
+ return ri.entity(new Object() {
+ @SuppressWarnings(value = "all")
+ public URI getUri() {
+
+ return newUriFromResource;
+ }
+ }).build();
+ }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/KillbillEventHandler.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/KillbillEventHandler.java
new file mode 100644
index 0000000..07dd7f8
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/KillbillEventHandler.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.util;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.util.userrequest.CompletionUserRequest;
+import org.killbill.billing.util.userrequest.CompletionUserRequestNotifier;
+
+import com.google.common.eventbus.Subscribe;
+
+public class KillbillEventHandler {
+
+
+ private final List<CompletionUserRequest> activeWaiters;
+
+ public KillbillEventHandler() {
+ activeWaiters = new LinkedList<CompletionUserRequest>();
+ }
+
+ public void registerCompletionUserRequestWaiter(final CompletionUserRequest waiter) {
+ if (waiter == null) {
+ return;
+ }
+ synchronized (activeWaiters) {
+ activeWaiters.add(waiter);
+ }
+ }
+
+ public void unregisterCompletionUserRequestWaiter(final CompletionUserRequest waiter) {
+ if (waiter == null) {
+ return;
+ }
+ synchronized (activeWaiters) {
+ activeWaiters.remove(waiter);
+ }
+ }
+
+ /*
+ * Killbill server event handler
+ */
+ @Subscribe
+ public void handleSubscriptionevents(final BusInternalEvent event) {
+ final List<CompletionUserRequestNotifier> runningWaiters = new ArrayList<CompletionUserRequestNotifier>();
+ synchronized (activeWaiters) {
+ runningWaiters.addAll(activeWaiters);
+ }
+ if (runningWaiters.size() == 0) {
+ return;
+ }
+ for (final CompletionUserRequestNotifier cur : runningWaiters) {
+ cur.onBusEvent(event);
+ }
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/glue/TestJaxrsModule.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/glue/TestJaxrsModule.java
new file mode 100644
index 0000000..0c7d52d
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/glue/TestJaxrsModule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.glue;
+
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+import com.google.inject.AbstractModule;
+
+public class TestJaxrsModule extends AbstractModule {
+
+ private void installObjectMapper() {
+ bind(com.fasterxml.jackson.databind.ObjectMapper.class).toInstance(new ObjectMapper());
+ }
+
+ @Override
+ protected void configure() {
+ installObjectMapper();
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/glue/TestJaxrsModuleNoDB.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/glue/TestJaxrsModuleNoDB.java
new file mode 100644
index 0000000..d2501ba
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/glue/TestJaxrsModuleNoDB.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.glue;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+
+public class TestJaxrsModuleNoDB extends TestJaxrsModule {
+
+ @Override
+ public void configure() {
+ super.configure();
+ install(new GuicyKillbillTestNoDBModule());
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/JaxrsTestSuiteNoDB.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/JaxrsTestSuiteNoDB.java
new file mode 100644
index 0000000..56bb2d2
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/JaxrsTestSuiteNoDB.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import org.testng.annotations.BeforeClass;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.billing.jaxrs.glue.TestJaxrsModuleNoDB;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public abstract class JaxrsTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ @Inject
+ protected ObjectMapper mapper;
+
+ @BeforeClass(groups = "fast")
+ protected void beforeClass() throws Exception {
+ final Injector injector = Guice.createInjector(new TestJaxrsModuleNoDB());
+ injector.injectMembers(this);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/JaxrsTestUtils.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/JaxrsTestUtils.java
new file mode 100644
index 0000000..c98e3ac
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/JaxrsTestUtils.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.jaxrs.json.AuditLogJson;
+
+public abstract class JaxrsTestUtils {
+
+ public static List<AuditLogJson> createAuditLogsJson(final DateTime changeDate) {
+ final List<AuditLogJson> auditLogs = new ArrayList<AuditLogJson>();
+ for (int i = 0; i < 20; i++) {
+ auditLogs.add(new AuditLogJson(UUID.randomUUID().toString(), changeDate, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(), UUID.randomUUID().toString()));
+ }
+
+ return auditLogs;
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAccountEmailJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAccountEmailJson.java
new file mode 100644
index 0000000..2161e12
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAccountEmailJson.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.AccountEmail;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+public class TestAccountEmailJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String accountId = UUID.randomUUID().toString();
+ final String email = UUID.randomUUID().toString();
+
+ final AccountEmailJson accountEmailJson = new AccountEmailJson(accountId, email);
+ Assert.assertEquals(accountEmailJson.getAccountId(), accountId);
+ Assert.assertEquals(accountEmailJson.getEmail(), email);
+
+ final String asJson = mapper.writeValueAsString(accountEmailJson);
+ Assert.assertEquals(asJson, "{\"accountId\":\"" + accountId + "\"," +
+ "\"email\":\"" + email + "\"," +
+ "\"auditLogs\":null}");
+
+ final AccountEmailJson fromJson = mapper.readValue(asJson, AccountEmailJson.class);
+ Assert.assertEquals(fromJson, accountEmailJson);
+ }
+
+ @Test(groups = "fast")
+ public void testToAccountEmail() throws Exception {
+ final String accountId = UUID.randomUUID().toString();
+ final String email = UUID.randomUUID().toString();
+
+ final AccountEmailJson accountEmailJson = new AccountEmailJson(accountId, email);
+ Assert.assertEquals(accountEmailJson.getAccountId(), accountId);
+ Assert.assertEquals(accountEmailJson.getEmail(), email);
+
+ final AccountEmail accountEmail = accountEmailJson.toAccountEmail(UUID.randomUUID());
+ Assert.assertEquals(accountEmail.getAccountId().toString(), accountId);
+ Assert.assertEquals(accountEmail.getEmail(), email);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAccountJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAccountJson.java
new file mode 100644
index 0000000..4a87407
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAccountJson.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.joda.time.DateTimeZone;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+import org.killbill.billing.mock.MockAccountBuilder;
+
+public class TestAccountJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String accountId = UUID.randomUUID().toString();
+ final String name = UUID.randomUUID().toString();
+ final Integer length = 12;
+ final String externalKey = UUID.randomUUID().toString();
+ final String email = UUID.randomUUID().toString();
+ final Integer billCycleDayLocal = 6;
+ final String currency = UUID.randomUUID().toString();
+ final String paymentMethodId = UUID.randomUUID().toString();
+ final String timeZone = UUID.randomUUID().toString();
+ final String address1 = UUID.randomUUID().toString();
+ final String address2 = UUID.randomUUID().toString();
+ final String postalCode = UUID.randomUUID().toString();
+ final String company = UUID.randomUUID().toString();
+ final String city = UUID.randomUUID().toString();
+ final String state = UUID.randomUUID().toString();
+ final String country = UUID.randomUUID().toString();
+ final String locale = UUID.randomUUID().toString();
+ final String phone = UUID.randomUUID().toString();
+ final Boolean isMigrated = true;
+ final Boolean isNotifiedForInvoice = false;
+
+ final AccountJson accountJson = new AccountJson(accountId, name, length, externalKey,
+ email, billCycleDayLocal, currency, paymentMethodId,
+ timeZone, address1, address2, postalCode, company, city, state,
+ country, locale, phone, isMigrated, isNotifiedForInvoice, null, null, null);
+ Assert.assertEquals(accountJson.getAccountId(), accountId);
+ Assert.assertEquals(accountJson.getName(), name);
+ Assert.assertEquals(accountJson.getFirstNameLength(), length);
+ Assert.assertEquals(accountJson.getExternalKey(), externalKey);
+ Assert.assertEquals(accountJson.getEmail(), email);
+ Assert.assertEquals(accountJson.getBillCycleDayLocal(), billCycleDayLocal);
+ Assert.assertEquals(accountJson.getCurrency(), currency);
+ Assert.assertEquals(accountJson.getPaymentMethodId(), paymentMethodId);
+ Assert.assertEquals(accountJson.getTimeZone(), timeZone);
+ Assert.assertEquals(accountJson.getAddress1(), address1);
+ Assert.assertEquals(accountJson.getAddress2(), address2);
+ Assert.assertEquals(accountJson.getPostalCode(), postalCode);
+ Assert.assertEquals(accountJson.getCompany(), company);
+ Assert.assertEquals(accountJson.getCity(), city);
+ Assert.assertEquals(accountJson.getState(), state);
+ Assert.assertEquals(accountJson.getCountry(), country);
+ Assert.assertEquals(accountJson.getLocale(), locale);
+ Assert.assertEquals(accountJson.getPhone(), phone);
+ Assert.assertEquals(accountJson.isMigrated(), isMigrated);
+ Assert.assertEquals(accountJson.isNotifiedForInvoices(), isNotifiedForInvoice);
+
+ final String asJson = mapper.writeValueAsString(accountJson);
+ final AccountJson fromJson = mapper.readValue(asJson, AccountJson.class);
+ Assert.assertEquals(fromJson, accountJson);
+ }
+
+ @Test(groups = "fast")
+ public void testFromAccount() throws Exception {
+ final MockAccountBuilder accountBuilder = new MockAccountBuilder();
+ accountBuilder.address1(UUID.randomUUID().toString());
+ accountBuilder.address2(UUID.randomUUID().toString());
+ final int bcd = 4;
+ accountBuilder.billingCycleDayLocal(bcd);
+ accountBuilder.city(UUID.randomUUID().toString());
+ accountBuilder.companyName(UUID.randomUUID().toString());
+ accountBuilder.country(UUID.randomUUID().toString());
+ accountBuilder.currency(Currency.GBP);
+ accountBuilder.email(UUID.randomUUID().toString());
+ accountBuilder.externalKey(UUID.randomUUID().toString());
+ accountBuilder.firstNameLength(12);
+ accountBuilder.isNotifiedForInvoices(true);
+ accountBuilder.locale(UUID.randomUUID().toString());
+ accountBuilder.migrated(true);
+ accountBuilder.name(UUID.randomUUID().toString());
+ accountBuilder.paymentMethodId(UUID.randomUUID());
+ accountBuilder.phone(UUID.randomUUID().toString());
+ accountBuilder.postalCode(UUID.randomUUID().toString());
+ accountBuilder.stateOrProvince(UUID.randomUUID().toString());
+ accountBuilder.timeZone(DateTimeZone.UTC);
+ final Account account = accountBuilder.build();
+
+ final AccountJson accountJson = new AccountJson(account, null, null, null);
+ Assert.assertEquals(accountJson.getAddress1(), account.getAddress1());
+ Assert.assertEquals(accountJson.getAddress2(), account.getAddress2());
+ Assert.assertEquals(accountJson.getBillCycleDayLocal(), (Integer) bcd);
+ Assert.assertEquals(accountJson.getCountry(), account.getCountry());
+ Assert.assertEquals(accountJson.getLocale(), account.getLocale());
+ Assert.assertEquals(accountJson.getCompany(), account.getCompanyName());
+ Assert.assertEquals(accountJson.getCity(), account.getCity());
+ Assert.assertEquals(accountJson.getCurrency(), account.getCurrency().toString());
+ Assert.assertEquals(accountJson.getEmail(), account.getEmail());
+ Assert.assertEquals(accountJson.getExternalKey(), account.getExternalKey());
+ Assert.assertEquals(accountJson.getName(), account.getName());
+ Assert.assertEquals(accountJson.getPaymentMethodId(), account.getPaymentMethodId().toString());
+ Assert.assertEquals(accountJson.getPhone(), account.getPhone());
+ Assert.assertEquals(accountJson.isMigrated(), account.isMigrated());
+ Assert.assertEquals(accountJson.isNotifiedForInvoices(), account.isNotifiedForInvoices());
+ Assert.assertEquals(accountJson.getState(), account.getStateOrProvince());
+ Assert.assertEquals(accountJson.getTimeZone(), account.getTimeZone().toString());
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAccountTimelineJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAccountTimelineJson.java
new file mode 100644
index 0000000..ecab945
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAccountTimelineJson.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+public class TestAccountTimelineJson extends JaxrsTestSuiteNoDB {
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAuditLogJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAuditLogJson.java
new file mode 100644
index 0000000..9687a04
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestAuditLogJson.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.audit.DefaultAuditLog;
+import org.killbill.billing.util.audit.dao.AuditLogModelDao;
+import org.killbill.billing.util.dao.EntityAudit;
+import org.killbill.billing.util.dao.TableName;
+
+public class TestAuditLogJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String changeType = UUID.randomUUID().toString();
+ final DateTime changeDate = clock.getUTCNow();
+ final String changedBy = UUID.randomUUID().toString();
+ final String reasonCode = UUID.randomUUID().toString();
+ final String comments = UUID.randomUUID().toString();
+ final String userToken = UUID.randomUUID().toString();
+
+ final AuditLogJson auditLogJson = new AuditLogJson(changeType, changeDate, changedBy, reasonCode, comments, userToken);
+ Assert.assertEquals(auditLogJson.getChangeType(), changeType);
+ Assert.assertEquals(auditLogJson.getChangeDate(), changeDate);
+ Assert.assertEquals(auditLogJson.getChangedBy(), changedBy);
+ Assert.assertEquals(auditLogJson.getReasonCode(), reasonCode);
+ Assert.assertEquals(auditLogJson.getComments(), comments);
+ Assert.assertEquals(auditLogJson.getUserToken(), userToken);
+
+ final String asJson = mapper.writeValueAsString(auditLogJson);
+ Assert.assertEquals(asJson, "{\"changeType\":\"" + auditLogJson.getChangeType() + "\"," +
+ "\"changeDate\":\"" + auditLogJson.getChangeDate().toDateTimeISO().toString() + "\"," +
+ "\"changedBy\":\"" + auditLogJson.getChangedBy() + "\"," +
+ "\"reasonCode\":\"" + auditLogJson.getReasonCode() + "\"," +
+ "\"comments\":\"" + auditLogJson.getComments() + "\"," +
+ "\"userToken\":\"" + auditLogJson.getUserToken() + "\"}");
+
+ final AuditLogJson fromJson = mapper.readValue(asJson, AuditLogJson.class);
+ Assert.assertEquals(fromJson, auditLogJson);
+ }
+
+ @Test(groups = "fast")
+ public void testConstructor() throws Exception {
+ final TableName tableName = TableName.ACCOUNT_EMAIL_HISTORY;
+ final long recordId = Long.MAX_VALUE;
+ final ChangeType changeType = ChangeType.DELETE;
+ final EntityAudit entityAudit = new EntityAudit(tableName, recordId, changeType, null);
+
+ final AuditLog auditLog = new DefaultAuditLog(new AuditLogModelDao(entityAudit, callContext), ObjectType.ACCOUNT_EMAIL, UUID.randomUUID());
+
+ final AuditLogJson auditLogJson = new AuditLogJson(auditLog);
+ Assert.assertEquals(auditLogJson.getChangeType(), changeType.toString());
+ Assert.assertNotNull(auditLogJson.getChangeDate());
+ Assert.assertEquals(auditLogJson.getChangedBy(), callContext.getUserName());
+ Assert.assertEquals(auditLogJson.getReasonCode(), callContext.getReasonCode());
+ Assert.assertEquals(auditLogJson.getComments(), callContext.getComments());
+ Assert.assertEquals(auditLogJson.getUserToken(), callContext.getUserToken().toString());
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBillingExceptionJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBillingExceptionJson.java
new file mode 100644
index 0000000..0625e6f
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBillingExceptionJson.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+import org.killbill.billing.jaxrs.json.BillingExceptionJson.StackTraceElementJson;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestBillingExceptionJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String className = UUID.randomUUID().toString();
+ final int code = Integer.MIN_VALUE;
+ final String message = UUID.randomUUID().toString();
+ final String causeClassName = UUID.randomUUID().toString();
+ final String causeMessage = UUID.randomUUID().toString();
+
+ final BillingExceptionJson exceptionJson = new BillingExceptionJson(className, code, message, causeClassName, causeMessage, ImmutableList.<StackTraceElementJson>of());
+ Assert.assertEquals(exceptionJson.getClassName(), className);
+ Assert.assertEquals(exceptionJson.getCode(), (Integer) code);
+ Assert.assertEquals(exceptionJson.getMessage(), message);
+ Assert.assertEquals(exceptionJson.getCauseClassName(), causeClassName);
+ Assert.assertEquals(exceptionJson.getCauseMessage(), causeMessage);
+ Assert.assertEquals(exceptionJson.getStackTrace().size(), 0);
+
+ final String asJson = mapper.writeValueAsString(exceptionJson);
+ final BillingExceptionJson fromJson = mapper.readValue(asJson, BillingExceptionJson.class);
+ Assert.assertEquals(fromJson, exceptionJson);
+ }
+
+ @Test(groups = "fast")
+ public void testFromException() throws Exception {
+ final String nil = null;
+ try {
+ nil.toString();
+ Assert.fail();
+ } catch (NullPointerException e) {
+ final BillingExceptionJson exceptionJson = new BillingExceptionJson(e);
+ Assert.assertEquals(exceptionJson.getClassName(), e.getClass().getName());
+ Assert.assertNull(exceptionJson.getCode());
+ Assert.assertNull(exceptionJson.getMessage());
+ Assert.assertNull(exceptionJson.getCauseClassName());
+ Assert.assertNull(exceptionJson.getCauseMessage());
+ Assert.assertFalse(exceptionJson.getStackTrace().isEmpty());
+ Assert.assertEquals(exceptionJson.getStackTrace().get(0).getClassName(), TestBillingExceptionJson.class.getName());
+ Assert.assertEquals(exceptionJson.getStackTrace().get(0).getMethodName(), "testFromException");
+ Assert.assertFalse(exceptionJson.getStackTrace().get(0).getNativeMethod());
+ }
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
new file mode 100644
index 0000000..c014465
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.jaxrs.json.SubscriptionJson.EventSubscriptionJson;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.killbill.billing.jaxrs.JaxrsTestUtils.createAuditLogsJson;
+
+public class TestBundleJsonWithSubscriptions extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+
+ final String someUUID = UUID.randomUUID().toString();
+ final UUID bundleId = UUID.randomUUID();
+ final String externalKey = UUID.randomUUID().toString();
+ final List<AuditLogJson> auditLogs = createAuditLogsJson(clock.getUTCNow());
+
+ EventSubscriptionJson event = new EventSubscriptionJson(someUUID, BillingPeriod.NO_BILLING_PERIOD.toString(), new LocalDate(), new LocalDate(), "product", "priceList", "eventType", "phase", null);
+ final SubscriptionJson subscription = new SubscriptionJson(someUUID, someUUID, someUUID, externalKey,
+ new LocalDate(), someUUID, someUUID, someUUID, someUUID, new LocalDate(), new LocalDate(),
+ new LocalDate(), new LocalDate(),
+ ImmutableList.<EventSubscriptionJson>of(event), null, null, auditLogs);
+
+ final BundleJson bundleJson = new BundleJson(someUUID, bundleId.toString(), externalKey, ImmutableList.<SubscriptionJson>of(subscription), auditLogs);
+ Assert.assertEquals(bundleJson.getBundleId(), bundleId.toString());
+ Assert.assertEquals(bundleJson.getExternalKey(), externalKey);
+ Assert.assertEquals(bundleJson.getSubscriptions().size(), 1);
+ Assert.assertEquals(bundleJson.getAuditLogs(), auditLogs);
+
+ final String asJson = mapper.writeValueAsString(bundleJson);
+ final BundleJson fromJson = mapper.readValue(asJson, BundleJson.class);
+ Assert.assertEquals(fromJson, bundleJson);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
new file mode 100644
index 0000000..ad73b3a
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestBundleTimelineJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String viewId = UUID.randomUUID().toString();
+ final String reason = UUID.randomUUID().toString();
+
+ final BundleJson bundleJson = createBundleWithSubscriptions();
+ final InvoiceJson invoiceJson = createInvoice();
+ final PaymentJson paymentJson = createPayment(UUID.fromString(invoiceJson.getAccountId()),
+ UUID.fromString(invoiceJson.getInvoiceId()));
+
+ final BundleTimelineJson bundleTimelineJson = new BundleTimelineJson(viewId,
+ bundleJson,
+ ImmutableList.<PaymentJson>of(paymentJson),
+ ImmutableList.<InvoiceJson>of(invoiceJson),
+ reason);
+
+ final String asJson = mapper.writeValueAsString(bundleTimelineJson);
+ final BundleTimelineJson fromJson = mapper.readValue(asJson, BundleTimelineJson.class);
+ Assert.assertEquals(fromJson, bundleTimelineJson);
+ }
+
+ private BundleJson createBundleWithSubscriptions() {
+ final String someUUID = UUID.randomUUID().toString();
+ final UUID accountId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final UUID subscriptionId = UUID.randomUUID();
+ final String externalKey = UUID.randomUUID().toString();
+
+ final SubscriptionJson entitlementJsonWithEvents = new SubscriptionJson(accountId.toString(), bundleId.toString(), subscriptionId.toString(), externalKey,
+ new LocalDate(), someUUID, someUUID, someUUID, someUUID,
+ new LocalDate(), new LocalDate(), new LocalDate(), new LocalDate(),
+ null, null, null, null);
+ return new BundleJson(accountId.toString(), bundleId.toString(), externalKey, ImmutableList.<SubscriptionJson>of(entitlementJsonWithEvents), null);
+ }
+
+ private InvoiceJson createInvoice() {
+ final UUID accountId = UUID.randomUUID();
+ final UUID invoiceId = UUID.randomUUID();
+ final BigDecimal invoiceAmount = BigDecimal.TEN;
+ final BigDecimal creditAdj = BigDecimal.ONE;
+ final BigDecimal refundAdj = BigDecimal.ONE;
+ final LocalDate invoiceDate = clock.getUTCToday();
+ final LocalDate targetDate = clock.getUTCToday();
+ final String invoiceNumber = UUID.randomUUID().toString();
+ final BigDecimal balance = BigDecimal.ZERO;
+
+ return new InvoiceJson(invoiceAmount, Currency.USD.toString(), creditAdj, refundAdj, invoiceId.toString(), invoiceDate,
+ targetDate, invoiceNumber, balance, accountId.toString(), null, null, null, null);
+ }
+
+ private PaymentJson createPayment(final UUID accountId, final UUID invoiceId) {
+ final UUID paymentId = UUID.randomUUID();
+ final Integer paymentNumber = 17;
+ final UUID paymentMethodId = UUID.randomUUID();
+ final BigDecimal paidAmount = BigDecimal.TEN;
+ final BigDecimal amount = BigDecimal.ZERO;
+ final DateTime paymentRequestedDate = clock.getUTCNow();
+ final DateTime paymentEffectiveDate = clock.getUTCNow();
+ final Integer retryCount = Integer.MAX_VALUE;
+ final String currency = "USD";
+ final String status = UUID.randomUUID().toString();
+ final String gatewayErrorCode = "OK";
+ final String gatewayErrorMsg = "Excellent...";
+ return new PaymentJson(amount, paidAmount, accountId.toString(), invoiceId.toString(), paymentId.toString(), paymentNumber.toString(),
+ paymentMethodId.toString(), paymentRequestedDate, paymentEffectiveDate, retryCount, currency, status,
+ gatewayErrorCode, gatewayErrorMsg, null, null, null, null);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestChargebackJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestChargebackJson.java
new file mode 100644
index 0000000..ee430ca
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestChargebackJson.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+import static org.killbill.billing.jaxrs.JaxrsTestUtils.createAuditLogsJson;
+
+public class TestChargebackJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final DateTime requestedDate = new DateTime(DateTimeZone.UTC);
+ final DateTime effectiveDate = new DateTime(DateTimeZone.UTC);
+ final BigDecimal chargebackAmount = BigDecimal.TEN;
+ final String chargebackId = UUID.randomUUID().toString();
+ final String accountId = UUID.randomUUID().toString();
+ final String paymentId = UUID.randomUUID().toString();
+ final String currency = "USD";
+ final List<AuditLogJson> auditLogs = createAuditLogsJson(clock.getUTCNow());
+ final ChargebackJson chargebackJson = new ChargebackJson(chargebackId, accountId, requestedDate, effectiveDate, chargebackAmount,
+ paymentId, currency, auditLogs);
+ Assert.assertEquals(chargebackJson.getChargebackId(), chargebackId);
+ Assert.assertEquals(chargebackJson.getRequestedDate(), requestedDate);
+ Assert.assertEquals(chargebackJson.getEffectiveDate(), effectiveDate);
+ Assert.assertEquals(chargebackJson.getAmount(), chargebackAmount);
+ Assert.assertEquals(chargebackJson.getPaymentId(), paymentId);
+ Assert.assertEquals(chargebackJson.getAuditLogs(), auditLogs);
+
+ final String asJson = mapper.writeValueAsString(chargebackJson);
+ final ChargebackJson fromJson = mapper.readValue(asJson, ChargebackJson.class);
+ Assert.assertEquals(fromJson, chargebackJson);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestCreditJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestCreditJson.java
new file mode 100644
index 0000000..c86a648
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestCreditJson.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+import static org.killbill.billing.jaxrs.JaxrsTestUtils.createAuditLogsJson;
+
+public class TestCreditJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final BigDecimal creditAmount = BigDecimal.TEN;
+ final String invoiceId = UUID.randomUUID().toString();
+ final String invoiceNumber = UUID.randomUUID().toString();
+ final LocalDate effectiveDate = clock.getUTCToday();
+ final String accountId = UUID.randomUUID().toString();
+ final List<AuditLogJson> auditLogs = createAuditLogsJson(clock.getUTCNow());
+ final CreditJson creditJson = new CreditJson(creditAmount, invoiceId, invoiceNumber, effectiveDate,
+ accountId, auditLogs);
+ Assert.assertEquals(creditJson.getEffectiveDate(), effectiveDate);
+ Assert.assertEquals(creditJson.getCreditAmount(), creditAmount);
+ Assert.assertEquals(creditJson.getInvoiceId(), invoiceId);
+ Assert.assertEquals(creditJson.getInvoiceNumber(), invoiceNumber);
+ Assert.assertEquals(creditJson.getAccountId(), accountId);
+
+ final String asJson = mapper.writeValueAsString(creditJson);
+ final CreditJson fromJson = mapper.readValue(asJson, CreditJson.class);
+ Assert.assertEquals(fromJson, creditJson);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestCustomFieldJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestCustomFieldJson.java
new file mode 100644
index 0000000..36edc38
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestCustomFieldJson.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+public class TestCustomFieldJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String customFieldId = UUID.randomUUID().toString();
+ final String objectId = UUID.randomUUID().toString();
+ final ObjectType objectType = ObjectType.INVOICE;
+ final String name = UUID.randomUUID().toString();
+ final String value = UUID.randomUUID().toString();
+ final CustomFieldJson customFieldJson = new CustomFieldJson(customFieldId, objectId, objectType, name, value, null);
+ Assert.assertEquals(customFieldJson.getCustomFieldId(), customFieldId);
+ Assert.assertEquals(customFieldJson.getObjectId(), objectId);
+ Assert.assertEquals(customFieldJson.getObjectType(), objectType);
+ Assert.assertEquals(customFieldJson.getName(), name);
+ Assert.assertEquals(customFieldJson.getValue(), value);
+ Assert.assertNull(customFieldJson.getAuditLogs());
+
+ final String asJson = mapper.writeValueAsString(customFieldJson);
+ final CustomFieldJson fromJson = mapper.readValue(asJson, CustomFieldJson.class);
+ Assert.assertEquals(fromJson, customFieldJson);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
new file mode 100644
index 0000000..7d7e648
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.clock.DefaultClock;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+import org.killbill.billing.jaxrs.json.SubscriptionJson.EventSubscriptionJson;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+
+import static org.killbill.billing.jaxrs.JaxrsTestUtils.createAuditLogsJson;
+
+public class TestEntitlementJsonWithEvents extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String someUUID = UUID.randomUUID().toString();
+ final String accountId = UUID.randomUUID().toString();
+ final String bundleId = UUID.randomUUID().toString();
+ final String subscriptionId = UUID.randomUUID().toString();
+ final String externalKey = UUID.randomUUID().toString();
+ final DateTime requestedDate = DefaultClock.toUTCDateTime(new DateTime(DateTimeZone.UTC));
+ final DateTime effectiveDate = DefaultClock.toUTCDateTime(new DateTime(DateTimeZone.UTC));
+ final UUID eventId = UUID.randomUUID();
+ final List<AuditLogJson> auditLogs = createAuditLogsJson(clock.getUTCNow());
+ final EventSubscriptionJson newEvent = new EventSubscriptionJson(eventId.toString(),
+ BillingPeriod.NO_BILLING_PERIOD.toString(),
+ requestedDate.toLocalDate(),
+ effectiveDate.toLocalDate(),
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ SubscriptionBaseTransitionType.CREATE.toString(),
+ PhaseType.DISCOUNT.toString(),
+ auditLogs);
+ final SubscriptionJson entitlementJsonWithEvents = new SubscriptionJson(accountId, bundleId, subscriptionId, externalKey,
+ new LocalDate(), someUUID, someUUID, someUUID, someUUID,
+ new LocalDate(), new LocalDate(), new LocalDate(), new LocalDate(),
+ null, null, null, null);
+
+
+ final String asJson = mapper.writeValueAsString(entitlementJsonWithEvents);
+
+ final SubscriptionJson fromJson = mapper.readValue(asJson, SubscriptionJson.class);
+ Assert.assertEquals(fromJson, entitlementJsonWithEvents);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceEmailJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceEmailJson.java
new file mode 100644
index 0000000..91786ba
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceEmailJson.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+public class TestInvoiceEmailJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String accountId = UUID.randomUUID().toString();
+ final boolean isNotifiedForInvoices = true;
+
+ final InvoiceEmailJson invoiceEmailJson = new InvoiceEmailJson(accountId, isNotifiedForInvoices);
+ Assert.assertEquals(invoiceEmailJson.getAccountId(), accountId);
+ Assert.assertEquals(invoiceEmailJson.isNotifiedForInvoices(), isNotifiedForInvoices);
+
+ final String asJson = mapper.writeValueAsString(invoiceEmailJson);
+ Assert.assertEquals(asJson, "{\"accountId\":\"" + accountId + "\"," +
+ "\"isNotifiedForInvoices\":" + isNotifiedForInvoices + "," +
+ "\"auditLogs\":null}");
+
+ final InvoiceEmailJson fromJson = mapper.readValue(asJson, InvoiceEmailJson.class);
+ Assert.assertEquals(fromJson, invoiceEmailJson);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceItemJsonSimple.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceItemJsonSimple.java
new file mode 100644
index 0000000..798c454
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceItemJsonSimple.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+import static org.killbill.billing.jaxrs.JaxrsTestUtils.createAuditLogsJson;
+
+public class TestInvoiceItemJsonSimple extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String invoiceItemId = UUID.randomUUID().toString();
+ final String invoiceId = UUID.randomUUID().toString();
+ final String linkedInvoiceItemId = UUID.randomUUID().toString();
+ final String accountId = UUID.randomUUID().toString();
+ final String bundleId = UUID.randomUUID().toString();
+ final String subscriptionId = UUID.randomUUID().toString();
+ final String planName = UUID.randomUUID().toString();
+ final String phaseName = UUID.randomUUID().toString();
+ final String type = "FIXED";
+ final String description = UUID.randomUUID().toString();
+ final LocalDate startDate = clock.getUTCToday();
+ final LocalDate endDate = clock.getUTCToday();
+ final BigDecimal amount = BigDecimal.TEN;
+ final Currency currency = Currency.MXN;
+ final List<AuditLogJson> auditLogs = createAuditLogsJson(clock.getUTCNow());
+ final InvoiceItemJson invoiceItemJson = new InvoiceItemJson(invoiceItemId, invoiceId, linkedInvoiceItemId, accountId,
+ bundleId, subscriptionId, planName, phaseName, type, description,
+ startDate, endDate, amount, currency, auditLogs);
+ Assert.assertEquals(invoiceItemJson.getInvoiceItemId(), invoiceItemId);
+ Assert.assertEquals(invoiceItemJson.getInvoiceId(), invoiceId);
+ Assert.assertEquals(invoiceItemJson.getLinkedInvoiceItemId(), linkedInvoiceItemId);
+ Assert.assertEquals(invoiceItemJson.getAccountId(), accountId);
+ Assert.assertEquals(invoiceItemJson.getBundleId(), bundleId);
+ Assert.assertEquals(invoiceItemJson.getSubscriptionId(), subscriptionId);
+ Assert.assertEquals(invoiceItemJson.getPlanName(), planName);
+ Assert.assertEquals(invoiceItemJson.getPhaseName(), phaseName);
+ Assert.assertEquals(invoiceItemJson.getItemType(), type);
+ Assert.assertEquals(invoiceItemJson.getDescription(), description);
+ Assert.assertEquals(invoiceItemJson.getStartDate(), startDate);
+ Assert.assertEquals(invoiceItemJson.getEndDate(), endDate);
+ Assert.assertEquals(invoiceItemJson.getAmount(), amount);
+ Assert.assertEquals(invoiceItemJson.getCurrency(), currency);
+ Assert.assertEquals(invoiceItemJson.getAuditLogs(), auditLogs);
+
+ final String asJson = mapper.writeValueAsString(invoiceItemJson);
+ final InvoiceItemJson fromJson = mapper.readValue(asJson, InvoiceItemJson.class);
+ Assert.assertEquals(fromJson, invoiceItemJson);
+ }
+
+ @Test(groups = "fast")
+ public void testFromInvoiceItem() throws Exception {
+ final InvoiceItem invoiceItem = Mockito.mock(InvoiceItem.class);
+ Mockito.when(invoiceItem.getId()).thenReturn(UUID.randomUUID());
+ Mockito.when(invoiceItem.getInvoiceId()).thenReturn(UUID.randomUUID());
+ Mockito.when(invoiceItem.getLinkedItemId()).thenReturn(UUID.randomUUID());
+ Mockito.when(invoiceItem.getAccountId()).thenReturn(UUID.randomUUID());
+ Mockito.when(invoiceItem.getBundleId()).thenReturn(UUID.randomUUID());
+ Mockito.when(invoiceItem.getSubscriptionId()).thenReturn(UUID.randomUUID());
+ Mockito.when(invoiceItem.getPlanName()).thenReturn(UUID.randomUUID().toString());
+ Mockito.when(invoiceItem.getPhaseName()).thenReturn(UUID.randomUUID().toString());
+ Mockito.when(invoiceItem.getDescription()).thenReturn(UUID.randomUUID().toString());
+ Mockito.when(invoiceItem.getStartDate()).thenReturn(clock.getUTCToday());
+ Mockito.when(invoiceItem.getEndDate()).thenReturn(clock.getUTCToday());
+ Mockito.when(invoiceItem.getAmount()).thenReturn(BigDecimal.TEN);
+ Mockito.when(invoiceItem.getCurrency()).thenReturn(Currency.EUR);
+ Mockito.when(invoiceItem.getInvoiceItemType()).thenReturn(InvoiceItemType.FIXED);
+
+ final InvoiceItemJson invoiceItemJson = new InvoiceItemJson(invoiceItem);
+ Assert.assertEquals(invoiceItemJson.getInvoiceItemId(), invoiceItem.getId().toString());
+ Assert.assertEquals(invoiceItemJson.getInvoiceId(), invoiceItem.getInvoiceId().toString());
+ Assert.assertEquals(invoiceItemJson.getLinkedInvoiceItemId(), invoiceItem.getLinkedItemId().toString());
+ Assert.assertEquals(invoiceItemJson.getAccountId(), invoiceItem.getAccountId().toString());
+ Assert.assertEquals(invoiceItemJson.getBundleId(), invoiceItem.getBundleId().toString());
+ Assert.assertEquals(invoiceItemJson.getSubscriptionId(), invoiceItem.getSubscriptionId().toString());
+ Assert.assertEquals(invoiceItemJson.getPlanName(), invoiceItem.getPlanName());
+ Assert.assertEquals(invoiceItemJson.getPhaseName(), invoiceItem.getPhaseName());
+ Assert.assertEquals(invoiceItemJson.getDescription(), invoiceItem.getDescription());
+ Assert.assertEquals(invoiceItemJson.getStartDate(), invoiceItem.getStartDate());
+ Assert.assertEquals(invoiceItemJson.getEndDate(), invoiceItem.getEndDate());
+ Assert.assertEquals(invoiceItemJson.getAmount(), invoiceItem.getAmount());
+ Assert.assertEquals(invoiceItemJson.getCurrency(), invoiceItem.getCurrency());
+ }
+}
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
new file mode 100644
index 0000000..2a38928
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.killbill.billing.jaxrs.JaxrsTestUtils.createAuditLogsJson;
+
+public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final BigDecimal amount = BigDecimal.TEN;
+ final BigDecimal creditAdj = BigDecimal.ONE;
+ final BigDecimal refundAdj = BigDecimal.ONE;
+ final String invoiceId = UUID.randomUUID().toString();
+ final LocalDate invoiceDate = clock.getUTCToday();
+ final LocalDate targetDate = clock.getUTCToday();
+ final String invoiceNumber = UUID.randomUUID().toString();
+ final BigDecimal balance = BigDecimal.ZERO;
+ final String accountId = UUID.randomUUID().toString();
+ final String bundleKeys = UUID.randomUUID().toString();
+ final CreditJson creditJson = createCreditJson();
+ final List<CreditJson> credits = ImmutableList.<CreditJson>of(creditJson);
+ final List<AuditLogJson> auditLogs = createAuditLogsJson(clock.getUTCNow());
+ final InvoiceJson invoiceJsonSimple = new InvoiceJson(amount, Currency.USD.toString(), creditAdj, refundAdj, invoiceId, invoiceDate,
+ targetDate, invoiceNumber, balance, accountId, bundleKeys,
+ credits, null, auditLogs);
+ Assert.assertEquals(invoiceJsonSimple.getAmount(), amount);
+ Assert.assertEquals(invoiceJsonSimple.getCreditAdj(), creditAdj);
+ Assert.assertEquals(invoiceJsonSimple.getRefundAdj(), refundAdj);
+ Assert.assertEquals(invoiceJsonSimple.getInvoiceId(), invoiceId);
+ Assert.assertEquals(invoiceJsonSimple.getInvoiceDate(), invoiceDate);
+ Assert.assertEquals(invoiceJsonSimple.getTargetDate(), targetDate);
+ Assert.assertEquals(invoiceJsonSimple.getInvoiceNumber(), invoiceNumber);
+ Assert.assertEquals(invoiceJsonSimple.getBalance(), balance);
+ Assert.assertEquals(invoiceJsonSimple.getAccountId(), accountId);
+ Assert.assertEquals(invoiceJsonSimple.getBundleKeys(), bundleKeys);
+ Assert.assertEquals(invoiceJsonSimple.getCredits(), credits);
+ Assert.assertEquals(invoiceJsonSimple.getAuditLogs(), auditLogs);
+
+ final String asJson = mapper.writeValueAsString(invoiceJsonSimple);
+ final InvoiceJson fromJson = mapper.readValue(asJson, InvoiceJson.class);
+ Assert.assertEquals(fromJson, invoiceJsonSimple);
+ }
+
+ @Test(groups = "fast")
+ public void testFromInvoice() throws Exception {
+ final Invoice invoice = Mockito.mock(Invoice.class);
+ Mockito.when(invoice.getChargedAmount()).thenReturn(BigDecimal.TEN);
+ Mockito.when(invoice.getCreditedAmount()).thenReturn(BigDecimal.ONE);
+ Mockito.when(invoice.getRefundedAmount()).thenReturn(BigDecimal.ONE);
+ Mockito.when(invoice.getId()).thenReturn(UUID.randomUUID());
+ Mockito.when(invoice.getInvoiceDate()).thenReturn(clock.getUTCToday());
+ Mockito.when(invoice.getTargetDate()).thenReturn(clock.getUTCToday());
+ Mockito.when(invoice.getInvoiceNumber()).thenReturn(Integer.MAX_VALUE);
+ Mockito.when(invoice.getBalance()).thenReturn(BigDecimal.ZERO);
+ Mockito.when(invoice.getAccountId()).thenReturn(UUID.randomUUID());
+ Mockito.when(invoice.getCurrency()).thenReturn(Currency.MXN);
+
+
+ final String bundleKeys = UUID.randomUUID().toString();
+ final List<CreditJson> credits = ImmutableList.<CreditJson>of(createCreditJson());
+
+ final InvoiceJson invoiceJson = new InvoiceJson(invoice, bundleKeys, credits, null);
+ Assert.assertEquals(invoiceJson.getAmount(), invoice.getChargedAmount());
+ Assert.assertEquals(invoiceJson.getCreditAdj(), invoice.getCreditedAmount());
+ Assert.assertEquals(invoiceJson.getRefundAdj(), invoice.getRefundedAmount());
+ Assert.assertEquals(invoiceJson.getInvoiceId(), invoice.getId().toString());
+ Assert.assertEquals(invoiceJson.getInvoiceDate(), invoice.getInvoiceDate());
+ Assert.assertEquals(invoiceJson.getTargetDate(), invoice.getTargetDate());
+ Assert.assertEquals(invoiceJson.getInvoiceNumber(), String.valueOf(invoice.getInvoiceNumber()));
+ Assert.assertEquals(invoiceJson.getBalance(), invoice.getBalance());
+ Assert.assertEquals(invoiceJson.getAccountId(), invoice.getAccountId().toString());
+ Assert.assertEquals(invoiceJson.getBundleKeys(), bundleKeys);
+ Assert.assertEquals(invoiceJson.getCredits(), credits);
+ Assert.assertNull(invoiceJson.getAuditLogs());
+ }
+
+ private CreditJson createCreditJson() {
+ final BigDecimal creditAmount = BigDecimal.TEN;
+ final String invoiceId = UUID.randomUUID().toString();
+ final String invoiceNumber = UUID.randomUUID().toString();
+ final LocalDate effectiveDate = clock.getUTCToday();
+ final String accountId = UUID.randomUUID().toString();
+ return new CreditJson(creditAmount, invoiceId, invoiceNumber, effectiveDate, accountId, null);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestOverdueStateJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestOverdueStateJson.java
new file mode 100644
index 0000000..a86d679
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestOverdueStateJson.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+public class TestOverdueStateJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String name = UUID.randomUUID().toString();
+ final String externalMessage = UUID.randomUUID().toString();
+ final int daysBetweenPaymentRetries = 12;
+ final boolean disableEntitlementAndChangesBlocked = true;
+ final boolean blockChanges = false;
+ final boolean clearState = true;
+ final int reevaluationIntervalDays = 100;
+ final OverdueStateJson overdueStateJson = new OverdueStateJson(name, externalMessage, daysBetweenPaymentRetries,
+ disableEntitlementAndChangesBlocked, blockChanges, clearState,
+ reevaluationIntervalDays);
+ Assert.assertEquals(overdueStateJson.getName(), name);
+ Assert.assertEquals(overdueStateJson.getExternalMessage(), externalMessage);
+ Assert.assertEquals(overdueStateJson.getDaysBetweenPaymentRetries(), (Integer) daysBetweenPaymentRetries);
+ Assert.assertEquals(overdueStateJson.isDisableEntitlementAndChangesBlocked(), (Boolean) disableEntitlementAndChangesBlocked);
+ Assert.assertEquals(overdueStateJson.isBlockChanges(), (Boolean) blockChanges);
+ Assert.assertEquals(overdueStateJson.isClearState(), (Boolean) clearState);
+ Assert.assertEquals(overdueStateJson.getReevaluationIntervalDays(), (Integer) reevaluationIntervalDays);
+
+ final String asJson = mapper.writeValueAsString(overdueStateJson);
+ final OverdueStateJson fromJson = mapper.readValue(asJson, OverdueStateJson.class);
+ Assert.assertEquals(fromJson, overdueStateJson);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestPlanDetailJason.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestPlanDetailJason.java
new file mode 100644
index 0000000..f15a544
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestPlanDetailJason.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.InternationalPrice;
+import org.killbill.billing.catalog.api.Listing;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+public class TestPlanDetailJason extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String productName = UUID.randomUUID().toString();
+ final String planName = UUID.randomUUID().toString();
+ final BillingPeriod billingPeriod = BillingPeriod.ANNUAL;
+ final String priceListName = UUID.randomUUID().toString();
+ final PlanDetailJson planDetailJason = new PlanDetailJson(productName, planName, billingPeriod, priceListName, null);
+ Assert.assertEquals(planDetailJason.getProductName(), productName);
+ Assert.assertEquals(planDetailJason.getPlanName(), planName);
+ Assert.assertEquals(planDetailJason.getBillingPeriod(), billingPeriod);
+ Assert.assertEquals(planDetailJason.getPriceListName(), priceListName);
+ Assert.assertEquals(planDetailJason.getFinalPhasePrice(), null);
+
+ final String asJson = mapper.writeValueAsString(planDetailJason);
+ Assert.assertEquals(asJson, "{\"productName\":\"" + planDetailJason.getProductName() + "\"," +
+ "\"planName\":\"" + planDetailJason.getPlanName() + "\"," +
+ "\"billingPeriod\":\"" + planDetailJason.getBillingPeriod().toString() + "\"," +
+ "\"priceListName\":\"" + planDetailJason.getPriceListName() + "\"," +
+ "\"finalPhasePrice\":null}");
+
+ final PlanDetailJson fromJson = mapper.readValue(asJson, PlanDetailJson.class);
+ Assert.assertEquals(fromJson, planDetailJason);
+ }
+
+ @Test(groups = "fast")
+ public void testFromListing() throws Exception {
+ final Product product = Mockito.mock(Product.class);
+ Mockito.when(product.getName()).thenReturn(UUID.randomUUID().toString());
+
+ final InternationalPrice price = Mockito.mock(InternationalPrice.class);
+ final PlanPhase planPhase = Mockito.mock(PlanPhase.class);
+ Mockito.when(planPhase.getRecurringPrice()).thenReturn(price);
+
+ final Plan plan = Mockito.mock(Plan.class);
+ Mockito.when(plan.getProduct()).thenReturn(product);
+ Mockito.when(plan.getName()).thenReturn(UUID.randomUUID().toString());
+ Mockito.when(plan.getBillingPeriod()).thenReturn(BillingPeriod.QUARTERLY);
+ Mockito.when(plan.getFinalPhase()).thenReturn(planPhase);
+
+ final PriceList priceList = Mockito.mock(PriceList.class);
+ Mockito.when(priceList.getName()).thenReturn(UUID.randomUUID().toString());
+
+ final Listing listing = Mockito.mock(Listing.class);
+ Mockito.when(listing.getPlan()).thenReturn(plan);
+ Mockito.when(listing.getPriceList()).thenReturn(priceList);
+
+ final PlanDetailJson planDetailJason = new PlanDetailJson(listing);
+ Assert.assertEquals(planDetailJason.getProductName(), plan.getProduct().getName());
+ Assert.assertEquals(planDetailJason.getPlanName(), plan.getName());
+ Assert.assertEquals(planDetailJason.getBillingPeriod(), plan.getBillingPeriod());
+ Assert.assertEquals(planDetailJason.getPriceListName(), priceList.getName());
+ Assert.assertEquals(planDetailJason.getFinalPhasePrice().size(), 0);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestRefundJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestRefundJson.java
new file mode 100644
index 0000000..2e13a40
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestRefundJson.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+import org.killbill.billing.payment.api.RefundStatus;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.killbill.billing.jaxrs.JaxrsTestUtils.createAuditLogsJson;
+
+public class TestRefundJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String refundId = UUID.randomUUID().toString();
+ final String paymentId = UUID.randomUUID().toString();
+ final BigDecimal amount = BigDecimal.TEN;
+ final String currency = "USD";
+ final boolean isAdjusted = true;
+ final DateTime requestedDate = clock.getUTCNow();
+ final DateTime effectiveDate = clock.getUTCNow();
+ final RefundStatus status = RefundStatus.COMPLETED;
+ final List<InvoiceItemJson> adjustments = ImmutableList.<InvoiceItemJson>of(createInvoiceItemJson());
+ final List<AuditLogJson> auditLogs = createAuditLogsJson(clock.getUTCNow());
+ final RefundJson refundJson = new RefundJson(refundId, paymentId, amount, currency, status.toString(), isAdjusted, requestedDate,
+ effectiveDate, adjustments, auditLogs);
+ Assert.assertEquals(refundJson.getRefundId(), refundId);
+ Assert.assertEquals(refundJson.getPaymentId(), paymentId);
+ Assert.assertEquals(refundJson.getAmount(), amount);
+ Assert.assertEquals(refundJson.getCurrency(), currency);
+ Assert.assertEquals(refundJson.isAdjusted(), isAdjusted);
+ Assert.assertEquals(refundJson.getRequestedDate(), requestedDate);
+ Assert.assertEquals(refundJson.getEffectiveDate(), effectiveDate);
+ Assert.assertEquals(refundJson.getAdjustments(), adjustments);
+ Assert.assertEquals(refundJson.getAuditLogs(), auditLogs);
+
+ final String asJson = mapper.writeValueAsString(refundJson);
+ final RefundJson fromJson = mapper.readValue(asJson, RefundJson.class);
+ Assert.assertEquals(fromJson, refundJson);
+ }
+
+ private InvoiceItemJson createInvoiceItemJson() {
+ final String invoiceItemId = UUID.randomUUID().toString();
+ final String invoiceId = UUID.randomUUID().toString();
+ final String linkedInvoiceItemId = UUID.randomUUID().toString();
+ final String accountId = UUID.randomUUID().toString();
+ final String bundleId = UUID.randomUUID().toString();
+ final String subscriptionId = UUID.randomUUID().toString();
+ final String planName = UUID.randomUUID().toString();
+ final String phaseName = UUID.randomUUID().toString();
+ final String description = UUID.randomUUID().toString();
+ final LocalDate startDate = clock.getUTCToday();
+ final LocalDate endDate = clock.getUTCToday();
+ final String type = "FIXED";
+ final BigDecimal amount = BigDecimal.TEN;
+ final Currency currency = Currency.MXN;
+ final List<AuditLogJson> auditLogs = createAuditLogsJson(clock.getUTCNow());
+ return new InvoiceItemJson(invoiceItemId, invoiceId, linkedInvoiceItemId, accountId, bundleId, subscriptionId,
+ planName, phaseName, type, description, startDate, endDate,
+ amount, currency, auditLogs);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestTagDefinitionJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestTagDefinitionJson.java
new file mode 100644
index 0000000..93721d6
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestTagDefinitionJson.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestTagDefinitionJson extends JaxrsTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String id = UUID.randomUUID().toString();
+ final Boolean isControlTag = true;
+ final String name = UUID.randomUUID().toString();
+ final String description = UUID.randomUUID().toString();
+ final ImmutableList<String> applicableObjectTypes = ImmutableList.<String>of(UUID.randomUUID().toString());
+ final TagDefinitionJson tagDefinitionJson = new TagDefinitionJson(id, isControlTag, name, description, applicableObjectTypes, null);
+ Assert.assertEquals(tagDefinitionJson.getId(), id);
+ Assert.assertEquals(tagDefinitionJson.isControlTag(), isControlTag);
+ Assert.assertEquals(tagDefinitionJson.getName(), name);
+ Assert.assertEquals(tagDefinitionJson.getDescription(), description);
+ Assert.assertEquals(tagDefinitionJson.getApplicableObjectTypes(), applicableObjectTypes);
+
+ final String asJson = mapper.writeValueAsString(tagDefinitionJson);
+ final TagDefinitionJson fromJson = mapper.readValue(asJson, TagDefinitionJson.class);
+ Assert.assertEquals(fromJson, tagDefinitionJson);
+ }
+}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/TestDateConversion.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/TestDateConversion.java
new file mode 100644
index 0000000..62e9ef0
--- /dev/null
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/TestDateConversion.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.jaxrs.resources.JaxRsResourceBase;
+
+public class TestDateConversion extends JaxRsResourceBase {
+
+ final UUID accountId = UUID.fromString("ffa649da-555e-4c55-bf65-84b06a4b3564");
+ final DateTimeZone dateTimeZone = DateTimeZone.forOffsetHours(-8);
+
+ public TestDateConversion() throws AccountApiException {
+ super(null, null, null, null, Mockito.mock(AccountUserApi.class), new ClockMock(), null);
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getTimeZone()).thenReturn(dateTimeZone);
+ Mockito.when(accountUserApi.getAccountById(accountId, null)).thenReturn(account);
+ }
+
+ @BeforeClass
+ public void beforeClass() {
+ }
+
+ @Test(groups = "fast")
+ public void testDateTimeConversion() {
+
+ final String input = "2013-08-26T06:50:20Z";
+ final LocalDate result = toLocalDate(accountId, input, null);
+ Assert.assertTrue(result.compareTo(new LocalDate(2013, 8, 25)) == 0);
+ }
+
+
+ @Test(groups = "fast")
+ public void testNullConversion() {
+ ((ClockMock) clock).setTime(new DateTime("2013-08-26T06:50:20Z"));
+ final String input = null;
+ final LocalDate result = toLocalDate(accountId, input, null);
+ Assert.assertTrue(result.compareTo(new LocalDate(2013, 8, 25)) == 0);
+ ((ClockMock) clock).resetDeltaFromReality();
+ }
+
+ @Test(groups = "fast")
+ public void testLocalDateConversion() {
+ final String input = "2013-08-25";
+ final LocalDate result = toLocalDate(accountId, input, null);
+ Assert.assertTrue(result.compareTo(new LocalDate(2013, 8, 25)) == 0);
+ }
+}
junction/pom.xml 76(+28 -48)
diff --git a/junction/pom.xml b/junction/pom.xml
index a240110..5db4c5e 100644
--- a/junction/pom.xml
+++ b/junction/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-junction</artifactId>
@@ -45,109 +45,89 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-java</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-entitlement</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-entitlement</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-subscription</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
diff --git a/junction/src/main/java/org/killbill/billing/junction/glue/DefaultJunctionModule.java b/junction/src/main/java/org/killbill/billing/junction/glue/DefaultJunctionModule.java
new file mode 100644
index 0000000..08a7259
--- /dev/null
+++ b/junction/src/main/java/org/killbill/billing/junction/glue/DefaultJunctionModule.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.glue;
+
+import com.google.inject.AbstractModule;
+import org.killbill.billing.glue.JunctionModule;
+import org.killbill.billing.junction.plumbing.billing.BlockingCalculator;
+import org.killbill.billing.junction.plumbing.billing.DefaultInternalBillingApi;
+import org.killbill.billing.junction.BillingInternalApi;
+import org.skife.config.ConfigSource;
+
+public class DefaultJunctionModule extends AbstractModule implements JunctionModule {
+
+ protected final ConfigSource configSource;
+
+ public DefaultJunctionModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ @Override
+ protected void configure() {
+ installBillingApi();
+ installBlockingCalculator();
+ }
+
+ @Override
+ public void installBillingApi() {
+ bind(BillingInternalApi.class).to(DefaultInternalBillingApi.class).asEagerSingleton();
+ }
+
+
+ public void installBlockingCalculator() {
+ bind(BlockingCalculator.class).asEagerSingleton();
+ }
+
+}
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java
new file mode 100644
index 0000000..dd33853
--- /dev/null
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BillCycleDayCalculator.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.plumbing.billing;
+
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.catalog.api.BillingAlignment;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.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.Product;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Inject;
+
+public class BillCycleDayCalculator {
+
+ private static final Logger log = LoggerFactory.getLogger(BillCycleDayCalculator.class);
+
+ private final CatalogService catalogService;
+ private final SubscriptionBaseInternalApi subscriptionApi;
+
+ @Inject
+ public BillCycleDayCalculator(final CatalogService catalogService, final SubscriptionBaseInternalApi subscriptionApi) {
+ this.catalogService = catalogService;
+ this.subscriptionApi = subscriptionApi;
+ }
+
+ protected int calculateBcd(final SubscriptionBaseBundle bundle, final SubscriptionBase subscription, final EffectiveSubscriptionInternalEvent transition, final Account account, final InternalCallContext context)
+ throws CatalogApiException, AccountApiException, SubscriptionBaseApiException {
+
+ final Catalog catalog = catalogService.getFullCatalog();
+
+ final Plan prevPlan = (transition.getPreviousPlan() != null) ? catalog.findPlan(transition.getPreviousPlan(), transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+ final Plan nextPlan = (transition.getNextPlan() != null) ? catalog.findPlan(transition.getNextPlan(), transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+
+ final Plan plan = (transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL) ? nextPlan : prevPlan;
+ final Product product = plan.getProduct();
+
+ final PlanPhase prevPhase = (transition.getPreviousPhase() != null) ? catalog.findPhase(transition.getPreviousPhase(), transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+ final PlanPhase nextPhase = (transition.getNextPhase() != null) ? catalog.findPhase(transition.getNextPhase(), transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+
+ final PlanPhase phase = (transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL) ? nextPhase : prevPhase;
+
+ final BillingAlignment alignment = catalog.billingAlignment(
+ new PlanPhaseSpecifier(product.getName(),
+ product.getCategory(),
+ phase.getBillingPeriod(),
+ transition.getNextPriceList(),
+ phase.getPhaseType()),
+ transition.getRequestedTransitionTime());
+
+ return calculateBcdForAlignment(alignment, bundle, subscription, account, catalog, plan, context);
+ }
+
+ @VisibleForTesting
+ int calculateBcdForAlignment(final BillingAlignment alignment, final SubscriptionBaseBundle bundle, final SubscriptionBase subscription,
+ final Account account, final Catalog catalog, final Plan plan, final InternalCallContext context) throws AccountApiException, SubscriptionBaseApiException, CatalogApiException {
+ int result = 0;
+ switch (alignment) {
+ case ACCOUNT:
+ result = account.getBillCycleDayLocal();
+ if (result == 0) {
+ result = calculateBcdFromSubscription(subscription, plan, account, catalog, context);
+ }
+ break;
+ case BUNDLE:
+ final SubscriptionBase baseSub = subscriptionApi.getBaseSubscription(bundle.getId(), context);
+ Plan basePlan = baseSub.getCurrentPlan();
+ if (basePlan == null) {
+ // The BP has been cancelled
+ basePlan = baseSub.getLastActivePlan();
+ }
+ result = calculateBcdFromSubscription(baseSub, basePlan, account, catalog, context);
+ break;
+ case SUBSCRIPTION:
+ result = calculateBcdFromSubscription(subscription, plan, account, catalog, context);
+ break;
+ }
+
+ if (result == 0) {
+ throw new CatalogApiException(ErrorCode.CAT_INVALID_BILLING_ALIGNMENT, alignment.toString());
+ }
+
+ return result;
+ }
+
+ @VisibleForTesting
+ int calculateBcdFromSubscription(final SubscriptionBase subscription, final Plan plan, final Account account, final Catalog catalog, final InternalCallContext context)
+ throws AccountApiException, CatalogApiException {
+ // Retrieve the initial phase type for that subscription
+ // TODO - this should be extracted somewhere, along with this code above
+ final PhaseType initialPhaseType;
+ final List<EffectiveSubscriptionInternalEvent> transitions = subscriptionApi.getAllTransitions(subscription, context);
+ if (transitions.size() == 0) {
+ initialPhaseType = null;
+ } else {
+ final DateTime requestedDate = subscription.getStartDate();
+ final String initialPhaseString = transitions.get(0).getNextPhase();
+ if (initialPhaseString == null) {
+ initialPhaseType = null;
+ } else {
+ final PlanPhase initialPhase = catalog.findPhase(initialPhaseString, requestedDate, subscription.getStartDate());
+ if (initialPhase == null) {
+ initialPhaseType = null;
+ } else {
+ initialPhaseType = initialPhase.getPhaseType();
+ }
+ }
+ }
+
+ final DateTime date = plan.dateOfFirstRecurringNonZeroCharge(subscription.getStartDate(), initialPhaseType);
+ final int bcdUTC = date.toDateTime(DateTimeZone.UTC).getDayOfMonth();
+ final int bcdLocal = date.toDateTime(account.getTimeZone()).getDayOfMonth();
+ log.info("Calculated BCD: subscription id {}, subscription start {}, timezone {}, bcd UTC {}, bcd local {}",
+ subscription.getId(), date.toDateTimeISO(), account.getTimeZone(), bcdUTC, bcdLocal);
+
+ return bcdLocal;
+ }
+}
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java
new file mode 100644
index 0000000..c651e18
--- /dev/null
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/BlockingCalculator.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.plumbing.billing;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingModeType;
+import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Inject;
+
+public class BlockingCalculator {
+
+ private static final AtomicLong globaltotalOrder = new AtomicLong();
+
+ private final BlockingInternalApi blockingApi;
+
+ protected static class DisabledDuration {
+
+ private final DateTime start;
+ private DateTime end;
+
+ public DisabledDuration(final DateTime start, final DateTime end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ public DateTime getStart() {
+ return start;
+ }
+
+ public DateTime getEnd() {
+ return end;
+ }
+
+ public void setEnd(final DateTime end) {
+ this.end = end;
+ }
+ }
+
+ @Inject
+ public BlockingCalculator(final BlockingInternalApi blockingApi) {
+ this.blockingApi = blockingApi;
+ }
+
+ /**
+ * Given a set of billing events, add corresponding blocking (overdue) billing events.
+ *
+ * @param billingEvents the original list of billing events to update (without overdue events)
+ */
+ public void insertBlockingEvents(final SortedSet<BillingEvent> billingEvents, final InternalTenantContext context) {
+ if (billingEvents.size() <= 0) {
+ return;
+ }
+
+ final Account account = billingEvents.first().getAccount();
+
+ final Hashtable<UUID, List<SubscriptionBase>> bundleMap = createBundleSubscriptionMap(billingEvents);
+
+ final SortedSet<BillingEvent> billingEventsToAdd = new TreeSet<BillingEvent>();
+ final SortedSet<BillingEvent> billingEventsToRemove = new TreeSet<BillingEvent>();
+
+ final List<BlockingState> blockingEvents = blockingApi.getBlockingAllForAccount(context);
+ final List<DisabledDuration> blockingDurations = createBlockingDurations(blockingEvents);
+ for (final UUID bundleId : bundleMap.keySet()) {
+ for (final SubscriptionBase subscription : bundleMap.get(bundleId)) {
+ billingEventsToAdd.addAll(createNewEvents(blockingDurations, billingEvents, account, subscription));
+ billingEventsToRemove.addAll(eventsToRemove(blockingDurations, billingEvents, subscription));
+ }
+ }
+
+ for (final BillingEvent eventToAdd : billingEventsToAdd) {
+ billingEvents.add(eventToAdd);
+ }
+
+ for (final BillingEvent eventToRemove : billingEventsToRemove) {
+ billingEvents.remove(eventToRemove);
+ }
+ }
+
+ protected SortedSet<BillingEvent> eventsToRemove(final List<DisabledDuration> disabledDuration,
+ final SortedSet<BillingEvent> billingEvents, final SubscriptionBase subscription) {
+ final SortedSet<BillingEvent> result = new TreeSet<BillingEvent>();
+
+ final SortedSet<BillingEvent> filteredBillingEvents = filter(billingEvents, subscription);
+ for (final DisabledDuration duration : disabledDuration) {
+ for (final BillingEvent event : filteredBillingEvents) {
+ if (duration.getEnd() == null || event.getEffectiveDate().isBefore(duration.getEnd())) {
+ if (event.getEffectiveDate().isAfter(duration.getStart())) { //between the pair
+ result.add(event);
+ }
+ } else { //after the last event of the pair no need to keep checking
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ protected SortedSet<BillingEvent> createNewEvents(final List<DisabledDuration> disabledDuration, final SortedSet<BillingEvent> billingEvents, final Account account, final SubscriptionBase subscription) {
+ final SortedSet<BillingEvent> result = new TreeSet<BillingEvent>();
+ for (final DisabledDuration duration : disabledDuration) {
+ // The first one before the blocked duration
+ final BillingEvent precedingInitialEvent = precedingBillingEventForSubscription(duration.getStart(), billingEvents, subscription);
+ // The last one during of before the duration
+ final BillingEvent precedingFinalEvent = precedingBillingEventForSubscription(duration.getEnd(), billingEvents, subscription);
+
+ if (precedingInitialEvent != null) { // there is a preceding billing event
+ result.add(createNewDisableEvent(duration.getStart(), precedingInitialEvent));
+ if (duration.getEnd() != null) { // no second event in the pair means they are still disabled (no re-enable)
+ result.add(createNewReenableEvent(duration.getEnd(), precedingFinalEvent));
+ }
+ } else if (precedingFinalEvent != null) { // can happen - e.g. phase event
+ result.add(createNewReenableEvent(duration.getEnd(), precedingFinalEvent));
+ }
+ // N.B. if there's no precedingInitial and no precedingFinal then there's nothing to do
+ }
+ return result;
+ }
+
+ protected BillingEvent precedingBillingEventForSubscription(final DateTime datetime, final SortedSet<BillingEvent> billingEvents, final SubscriptionBase subscription) {
+ if (datetime == null) { //second of a pair can be null if there's no re-enabling
+ return null;
+ }
+
+ final SortedSet<BillingEvent> filteredBillingEvents = filter(billingEvents, subscription);
+ BillingEvent result = filteredBillingEvents.first();
+
+ if (datetime.isBefore(result.getEffectiveDate())) {
+ //This case can happen, for example, if we have an add on and the bundle goes into disabled before the add on is created
+ return null;
+ }
+
+ for (final BillingEvent event : filteredBillingEvents) {
+ if (!event.getEffectiveDate().isBefore(datetime)) { // found it its the previous event
+ return result;
+ } else { // still looking
+ result = event;
+ }
+ }
+ return result;
+ }
+
+ protected SortedSet<BillingEvent> filter(final SortedSet<BillingEvent> billingEvents, final SubscriptionBase subscription) {
+ final SortedSet<BillingEvent> result = new TreeSet<BillingEvent>();
+ for (final BillingEvent event : billingEvents) {
+ if (event.getSubscription() == subscription) {
+ result.add(event);
+ }
+ }
+ return result;
+ }
+
+ protected BillingEvent createNewDisableEvent(final DateTime odEventTime, final BillingEvent previousEvent) {
+ final Account account = previousEvent.getAccount();
+ final int billCycleDay = previousEvent.getBillCycleDayLocal();
+ final SubscriptionBase subscription = previousEvent.getSubscription();
+ final DateTime effectiveDate = odEventTime;
+ final PlanPhase planPhase = previousEvent.getPlanPhase();
+ final Plan plan = previousEvent.getPlan();
+
+ // Make sure to set the fixed price to null and the billing period to NO_BILLING_PERIOD,
+ // which makes invoice disregard this event
+ final BigDecimal fixedPrice = null;
+ final BigDecimal recurringPrice = null;
+ final BillingPeriod billingPeriod = BillingPeriod.NO_BILLING_PERIOD;
+
+ final Currency currency = previousEvent.getCurrency();
+ final String description = "";
+ final BillingModeType billingModeType = previousEvent.getBillingMode();
+ final SubscriptionBaseTransitionType type = SubscriptionBaseTransitionType.START_BILLING_DISABLED;
+ final Long totalOrdering = globaltotalOrder.getAndIncrement();
+ final DateTimeZone tz = previousEvent.getTimeZone();
+
+ return new DefaultBillingEvent(account, subscription, effectiveDate, plan, planPhase,
+ fixedPrice, recurringPrice, currency,
+ billingPeriod, billCycleDay, billingModeType,
+ description, totalOrdering, type, tz);
+ }
+
+ protected BillingEvent createNewReenableEvent(final DateTime odEventTime, final BillingEvent previousEvent) {
+ // All fields are populated with the event state from before the blocking period, for invoice to resume invoicing
+ final Account account = previousEvent.getAccount();
+ final int billCycleDay = previousEvent.getBillCycleDayLocal();
+ final SubscriptionBase subscription = previousEvent.getSubscription();
+ final DateTime effectiveDate = odEventTime;
+ final PlanPhase planPhase = previousEvent.getPlanPhase();
+ final Plan plan = previousEvent.getPlan();
+ final BigDecimal fixedPrice = previousEvent.getFixedPrice();
+ final BigDecimal recurringPrice = previousEvent.getRecurringPrice();
+ final Currency currency = previousEvent.getCurrency();
+ final String description = "";
+ final BillingModeType billingModeType = previousEvent.getBillingMode();
+ final BillingPeriod billingPeriod = previousEvent.getBillingPeriod();
+ final SubscriptionBaseTransitionType type = SubscriptionBaseTransitionType.END_BILLING_DISABLED;
+ final Long totalOrdering = globaltotalOrder.getAndIncrement();
+ final DateTimeZone tz = previousEvent.getTimeZone();
+
+ return new DefaultBillingEvent(account, subscription, effectiveDate, plan, planPhase,
+ fixedPrice, recurringPrice, currency,
+ billingPeriod, billCycleDay, billingModeType,
+ description, totalOrdering, type, tz);
+ }
+
+ protected Hashtable<UUID, List<SubscriptionBase>> createBundleSubscriptionMap(final SortedSet<BillingEvent> billingEvents) {
+ final Hashtable<UUID, List<SubscriptionBase>> result = new Hashtable<UUID, List<SubscriptionBase>>();
+ for (final BillingEvent event : billingEvents) {
+ final UUID bundleId = event.getSubscription().getBundleId();
+ List<SubscriptionBase> subs = result.get(bundleId);
+ if (subs == null) {
+ subs = new ArrayList<SubscriptionBase>();
+ result.put(bundleId, subs);
+ }
+ if (!result.get(bundleId).contains(event.getSubscription())) {
+ subs.add(event.getSubscription());
+ }
+ }
+ return result;
+ }
+
+ // In ascending order
+ protected List<DisabledDuration> createBlockingDurations(final Iterable<BlockingState> overdueBundleEvents) {
+ final List<DisabledDuration> result = new ArrayList<BlockingCalculator.DisabledDuration>();
+ // Earliest blocking event
+ BlockingState first = null;
+
+ int blockedNesting = 0;
+ BlockingState lastOne = null;
+ for (final BlockingState e : overdueBundleEvents) {
+ lastOne = e;
+ if (e.isBlockBilling() && blockedNesting == 0) {
+ // First blocking event of contiguous series of blocking events
+ first = e;
+ blockedNesting++;
+ } else if (e.isBlockBilling() && blockedNesting > 0) {
+ // Nest blocking states
+ blockedNesting++;
+ } else if (!e.isBlockBilling() && blockedNesting > 0) {
+ blockedNesting--;
+ if (blockedNesting == 0) {
+ // End of the interval
+ addDisabledDuration(result, first, e);
+ first = null;
+ }
+ }
+ }
+
+ if (first != null) { // found a transition to disabled with no terminating event
+ addDisabledDuration(result, first, lastOne.isBlockBilling() ? null : lastOne);
+ }
+
+ return result;
+ }
+
+ private void addDisabledDuration(final List<DisabledDuration> result, final BlockingState firstBlocking, @Nullable final BlockingState firstNonBlocking) {
+ final DisabledDuration lastOne;
+ if (!result.isEmpty()) {
+ lastOne = result.get(result.size() - 1);
+ } else {
+ lastOne = null;
+ }
+
+ final DateTime endDate = firstNonBlocking == null ? null : firstNonBlocking.getEffectiveDate();
+ if (lastOne != null && lastOne.getEnd().compareTo(firstBlocking.getEffectiveDate()) == 0) {
+ lastOne.setEnd(endDate);
+ } else {
+ result.add(new DisabledDuration(firstBlocking.getEffectiveDate(), endDate));
+ }
+ }
+
+ @VisibleForTesting
+ static AtomicLong getGlobalTotalOrder() {
+ return globaltotalOrder;
+ }
+}
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
new file mode 100644
index 0000000..10fea39
--- /dev/null
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.plumbing.billing;
+
+import java.math.BigDecimal;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingModeType;
+
+public class DefaultBillingEvent implements BillingEvent {
+ private final Account account;
+ private final int billCycleDayLocal;
+ private final SubscriptionBase subscription;
+ private final DateTime effectiveDate;
+ private final PlanPhase planPhase;
+ private final Plan plan;
+ private final BigDecimal fixedPrice;
+ private final BigDecimal recurringPrice;
+ private final Currency currency;
+ private final String description;
+ private final BillingModeType billingModeType;
+ private final BillingPeriod billingPeriod;
+ private final SubscriptionBaseTransitionType type;
+ private final Long totalOrdering;
+ private final DateTimeZone timeZone;
+
+ public DefaultBillingEvent(final Account account, final EffectiveSubscriptionInternalEvent transition, final SubscriptionBase subscription, final int billCycleDayLocal, final Currency currency, final Catalog catalog) throws CatalogApiException {
+
+ this.account = account;
+ this.billCycleDayLocal = billCycleDayLocal;
+ this.subscription = subscription;
+ effectiveDate = transition.getEffectiveTransitionTime();
+ final String planPhaseName = (transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL) ?
+ transition.getNextPhase() : transition.getPreviousPhase();
+ planPhase = (planPhaseName != null) ? catalog.findPhase(planPhaseName, transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+
+ final String planName = (transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL) ?
+ transition.getNextPlan() : transition.getPreviousPlan();
+ plan = (planName != null) ? catalog.findPlan(planName, transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+
+ final String nextPhaseName = transition.getNextPhase();
+ final PlanPhase nextPhase = (nextPhaseName != null) ? catalog.findPhase(nextPhaseName, transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+
+ final String prevPhaseName = transition.getPreviousPhase();
+ final PlanPhase prevPhase = (prevPhaseName != null) ? catalog.findPhase(prevPhaseName, transition.getEffectiveTransitionTime(), transition.getSubscriptionStartDate()) : null;
+
+
+ fixedPrice = (nextPhase != null && nextPhase.getFixedPrice() != null) ? nextPhase.getFixedPrice().getPrice(currency) : null;
+ recurringPrice = (nextPhase != null && nextPhase.getRecurringPrice() != null) ? nextPhase.getRecurringPrice().getPrice(currency) : null;
+
+ this.currency = currency;
+ description = transition.getTransitionType().toString();
+ billingModeType = BillingModeType.IN_ADVANCE;
+ billingPeriod = (transition.getTransitionType() != SubscriptionBaseTransitionType.CANCEL) ?
+ nextPhase.getBillingPeriod() : prevPhase.getBillingPeriod();
+ type = transition.getTransitionType();
+ totalOrdering = transition.getTotalOrdering();
+ timeZone = account.getTimeZone();
+ }
+
+ public DefaultBillingEvent(final Account account, final SubscriptionBase subscription, final DateTime effectiveDate, final Plan plan, final PlanPhase planPhase,
+ final BigDecimal fixedPrice, final BigDecimal recurringPrice, final Currency currency,
+ final BillingPeriod billingPeriod, final int billCycleDayLocal, final BillingModeType billingModeType,
+ final String description, final long totalOrdering, final SubscriptionBaseTransitionType type, final DateTimeZone timeZone) {
+ this.account = account;
+ this.subscription = subscription;
+ this.effectiveDate = effectiveDate;
+ this.plan = plan;
+ this.planPhase = planPhase;
+ this.fixedPrice = fixedPrice;
+ this.recurringPrice = recurringPrice;
+ this.currency = currency;
+ this.billingPeriod = billingPeriod;
+ this.billCycleDayLocal = billCycleDayLocal;
+ this.billingModeType = billingModeType;
+ this.description = description;
+ this.type = type;
+ this.totalOrdering = totalOrdering;
+ this.timeZone = timeZone;
+ }
+
+ @Override
+ public int compareTo(final BillingEvent e1) {
+ if (!getSubscription().getId().equals(e1.getSubscription().getId())) { // First order by subscription
+ return getSubscription().getId().compareTo(e1.getSubscription().getId());
+ } else { // subscriptions are the same
+ if (!getEffectiveDate().equals(e1.getEffectiveDate())) { // Secondly order by date
+ return getEffectiveDate().compareTo(e1.getEffectiveDate());
+ } else { // dates and subscriptions are the same
+ // If an subscription event and an overdue event happen at the exact same time,
+ // we assume we want the subscription event before the overdue event when entering
+ // the overdue period, and vice-versa when exiting the overdue period
+ if (SubscriptionBaseTransitionType.START_BILLING_DISABLED.equals(getTransitionType())) {
+ if (SubscriptionBaseTransitionType.END_BILLING_DISABLED.equals(e1.getTransitionType())) {
+ // Make sure to always have START before END
+ return -1;
+ } else {
+ return 1;
+ }
+ } else if (SubscriptionBaseTransitionType.START_BILLING_DISABLED.equals(e1.getTransitionType())) {
+ if (SubscriptionBaseTransitionType.END_BILLING_DISABLED.equals(getTransitionType())) {
+ // Make sure to always have START before END
+ return 1;
+ } else {
+ return -1;
+ }
+ } else if (SubscriptionBaseTransitionType.END_BILLING_DISABLED.equals(getTransitionType())) {
+ if (SubscriptionBaseTransitionType.START_BILLING_DISABLED.equals(e1.getTransitionType())) {
+ // Make sure to always have START before END
+ return 1;
+ } else {
+ return -1;
+ }
+ } else if (SubscriptionBaseTransitionType.END_BILLING_DISABLED.equals(e1.getTransitionType())) {
+ if (SubscriptionBaseTransitionType.START_BILLING_DISABLED.equals(getTransitionType())) {
+ // Make sure to always have START before END
+ return -1;
+ } else {
+ return 1;
+ }
+ } else {
+ return getTotalOrdering().compareTo(e1.getTotalOrdering());
+ }
+ }
+ }
+ }
+
+ @Override
+ public Account getAccount() {
+ return account;
+ }
+
+ @Override
+ public int getBillCycleDayLocal() {
+ return billCycleDayLocal;
+ }
+
+ @Override
+ public SubscriptionBase getSubscription() {
+ return subscription;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public PlanPhase getPlanPhase() {
+ return planPhase;
+ }
+
+ @Override
+ public Plan getPlan() {
+ return plan;
+ }
+
+ @Override
+ public BillingPeriod getBillingPeriod() {
+ return billingPeriod;
+ }
+
+ @Override
+ public BillingModeType getBillingMode() {
+ return billingModeType;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public BigDecimal getFixedPrice() {
+ return fixedPrice;
+ }
+
+ @Override
+ public BigDecimal getRecurringPrice() {
+ return recurringPrice;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public SubscriptionBaseTransitionType getTransitionType() {
+ return type;
+ }
+
+ @Override
+ public Long getTotalOrdering() {
+ return totalOrdering;
+ }
+
+ @Override
+ public String toString() {
+ // Note: we don't use all fields here, as the output would be overwhelming
+ // (these events are printed in the logs in junction and invoice).
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultBillingEvent");
+ sb.append("{type=").append(type);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", planPhaseName=").append(planPhase.getName());
+ sb.append(", subscriptionId=").append(subscription.getId());
+ sb.append(", totalOrdering=").append(totalOrdering);
+ sb.append(", accountId=").append(account.getId());
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultBillingEvent that = (DefaultBillingEvent) o;
+
+ if (billCycleDayLocal != that.billCycleDayLocal) {
+ return false;
+ }
+ if (account != null ? !account.equals(that.account) : that.account != null) {
+ return false;
+ }
+ if (billingModeType != that.billingModeType) {
+ return false;
+ }
+ if (billingPeriod != that.billingPeriod) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (description != null ? !description.equals(that.description) : that.description != null) {
+ return false;
+ }
+ if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+ return false;
+ }
+ if (fixedPrice != null ? !fixedPrice.equals(that.fixedPrice) : that.fixedPrice != null) {
+ return false;
+ }
+ if (plan != null ? !plan.equals(that.plan) : that.plan != null) {
+ return false;
+ }
+ if (planPhase != null ? !planPhase.equals(that.planPhase) : that.planPhase != null) {
+ return false;
+ }
+ if (recurringPrice != null ? !recurringPrice.equals(that.recurringPrice) : that.recurringPrice != null) {
+ return false;
+ }
+ if (subscription != null ? !subscription.equals(that.subscription) : that.subscription != null) {
+ return false;
+ }
+ if (timeZone != null ? !timeZone.equals(that.timeZone) : that.timeZone != null) {
+ return false;
+ }
+ if (totalOrdering != null ? !totalOrdering.equals(that.totalOrdering) : that.totalOrdering != null) {
+ return false;
+ }
+ if (type != that.type) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = account != null ? account.hashCode() : 0;
+ result = 31 * result + billCycleDayLocal;
+ result = 31 * result + (subscription != null ? subscription.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (planPhase != null ? planPhase.hashCode() : 0);
+ result = 31 * result + (plan != null ? plan.hashCode() : 0);
+ result = 31 * result + (fixedPrice != null ? fixedPrice.hashCode() : 0);
+ result = 31 * result + (recurringPrice != null ? recurringPrice.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (billingModeType != null ? billingModeType.hashCode() : 0);
+ result = 31 * result + (billingPeriod != null ? billingPeriod.hashCode() : 0);
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ result = 31 * result + (totalOrdering != null ? totalOrdering.hashCode() : 0);
+ result = 31 * result + (timeZone != null ? timeZone.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public DateTimeZone getTimeZone() {
+ return timeZone;
+ }
+}
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
new file mode 100644
index 0000000..4b59c9b
--- /dev/null
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEventSet.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.plumbing.billing;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+
+public class DefaultBillingEventSet extends TreeSet<BillingEvent> implements SortedSet<BillingEvent>, BillingEventSet {
+ private static final long serialVersionUID = 1L;
+
+ private boolean accountAutoInvoiceOff = false;
+ private List<UUID> subscriptionIdsWithAutoInvoiceOff = new ArrayList<UUID>();
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.junction.plumbing.billing.BillingEventSet#isAccountAutoInvoiceOff()
+ */
+ @Override
+ public boolean isAccountAutoInvoiceOff() {
+ return accountAutoInvoiceOff;
+ }
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.junction.plumbing.billing.BillingEventSet#getSubscriptionIdsWithAutoInvoiceOff()
+ */
+ @Override
+ public List<UUID> getSubscriptionIdsWithAutoInvoiceOff() {
+ return subscriptionIdsWithAutoInvoiceOff;
+ }
+
+ public void setAccountAutoInvoiceIsOff(final boolean accountAutoInvoiceIsOff) {
+ this.accountAutoInvoiceOff = accountAutoInvoiceIsOff;
+ }
+
+ public void setSubscriptionIdsWithAutoInvoiceOff(final List<UUID> subscriptionIdsWithAutoInvoiceOff) {
+ this.subscriptionIdsWithAutoInvoiceOff = subscriptionIdsWithAutoInvoiceOff;
+ }
+
+ @Override
+ public String toString() {
+ return "DefaultBillingEventSet [accountAutoInvoiceOff=" + accountAutoInvoiceOff
+ + ", subscriptionIdsWithAutoInvoiceOff=" + subscriptionIdsWithAutoInvoiceOff + ", Events="
+ + super.toString() + "]";
+ }
+
+
+}
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
new file mode 100644
index 0000000..01d1d3c
--- /dev/null
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultInternalBillingApi.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.plumbing.billing;
+
+import java.util.List;
+import java.util.SortedSet;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.MutableAccountData;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+import org.killbill.billing.junction.BillingInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.Tag;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.inject.Inject;
+
+public class DefaultInternalBillingApi implements BillingInternalApi {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultInternalBillingApi.class);
+ private final AccountInternalApi accountApi;
+ private final BillCycleDayCalculator bcdCalculator;
+ private final SubscriptionBaseInternalApi subscriptionApi;
+ private final CatalogService catalogService;
+ private final BlockingCalculator blockCalculator;
+ private final TagInternalApi tagApi;
+
+ @Inject
+ public DefaultInternalBillingApi(final AccountInternalApi accountApi,
+ final BillCycleDayCalculator bcdCalculator,
+ final SubscriptionBaseInternalApi subscriptionApi,
+ final BlockingCalculator blockCalculator,
+ final CatalogService catalogService, final TagInternalApi tagApi) {
+ this.accountApi = accountApi;
+ this.bcdCalculator = bcdCalculator;
+ this.subscriptionApi = subscriptionApi;
+ this.catalogService = catalogService;
+ this.blockCalculator = blockCalculator;
+ this.tagApi = tagApi;
+ }
+
+ @Override
+ public BillingEventSet getBillingEventsForAccountAndUpdateAccountBCD(final UUID accountId, final InternalCallContext context) {
+ final List<SubscriptionBaseBundle> bundles = subscriptionApi.getBundlesForAccount(accountId, context);
+ final DefaultBillingEventSet result = new DefaultBillingEventSet();
+
+ try {
+ final Account account = accountApi.getAccountById(accountId, context);
+
+ // Check to see if billing is off for the account
+ final List<Tag> accountTags = tagApi.getTags(accountId, ObjectType.ACCOUNT, context);
+ final boolean found_AUTO_INVOICING_OFF = is_AUTO_INVOICING_OFF(accountTags);
+ if (found_AUTO_INVOICING_OFF) {
+ result.setAccountAutoInvoiceIsOff(true);
+ return result; // billing is off, we are done
+ }
+
+ addBillingEventsForBundles(bundles, account, context, result);
+ } catch (AccountApiException e) {
+ log.warn("Failed while getting BillingEvent", e);
+ }
+
+ // Pretty-print the events, before and after the blocking calculator does its magic
+ final StringBuilder logStringBuilder = new StringBuilder("Computed billing events for accountId ").append(accountId);
+ eventsToString(logStringBuilder, result, "\nBilling Events Raw");
+ blockCalculator.insertBlockingEvents(result, context);
+ eventsToString(logStringBuilder, result, "\nBilling Events After Blocking");
+ log.info(logStringBuilder.toString());
+
+ return result;
+ }
+
+ private void eventsToString(final StringBuilder stringBuilder, final SortedSet<BillingEvent> events, final String title) {
+ stringBuilder.append(title);
+ for (final BillingEvent event : events) {
+ stringBuilder.append("\n").append(event.toString());
+ }
+ }
+
+ private void addBillingEventsForBundles(final List<SubscriptionBaseBundle> bundles, final Account account, final InternalCallContext context,
+ final DefaultBillingEventSet result) {
+ for (final SubscriptionBaseBundle bundle : bundles) {
+ final List<SubscriptionBase> subscriptions = subscriptionApi.getSubscriptionsForBundle(bundle.getId(), context);
+
+ //Check if billing is off for the bundle
+ final List<Tag> bundleTags = tagApi.getTags(bundle.getId(), ObjectType.BUNDLE, context);
+ boolean found_AUTO_INVOICING_OFF = is_AUTO_INVOICING_OFF(bundleTags);
+ if (found_AUTO_INVOICING_OFF) {
+ for (final SubscriptionBase subscription : subscriptions) { // billing is off so list sub ids in set to be excluded
+ result.getSubscriptionIdsWithAutoInvoiceOff().add(subscription.getId());
+ }
+ } else { // billing is not off
+ addBillingEventsForSubscription(subscriptions, bundle, account, context, result);
+ }
+ }
+ }
+
+ private void addBillingEventsForSubscription(final List<SubscriptionBase> subscriptions, final SubscriptionBaseBundle bundle, final Account account, final InternalCallContext context, final DefaultBillingEventSet result) {
+
+ boolean updatedAccountBCD = false;
+ for (final SubscriptionBase subscription : subscriptions) {
+ for (final EffectiveSubscriptionInternalEvent transition : subscriptionApi.getBillingTransitions(subscription, context)) {
+ try {
+ final int bcdLocal = bcdCalculator.calculateBcd(bundle, subscription, transition, account, context);
+
+ if (account.getBillCycleDayLocal() == 0 && !updatedAccountBCD) {
+ final MutableAccountData modifiedData = account.toMutableAccountData();
+ modifiedData.setBillCycleDayLocal(bcdLocal);
+ accountApi.updateAccount(account.getExternalKey(), modifiedData, context);
+ updatedAccountBCD = true;
+ }
+
+ final BillingEvent event = new DefaultBillingEvent(account, transition, subscription, bcdLocal, account.getCurrency(), catalogService.getFullCatalog());
+ result.add(event);
+ } catch (CatalogApiException e) {
+ log.error("Failing to identify catalog components while creating BillingEvent from transition: " +
+ transition.getId().toString(), e);
+ } catch (Exception e) {
+ log.warn("Failed while getting BillingEvent", e);
+ }
+ }
+ }
+ }
+
+ private final boolean is_AUTO_INVOICING_OFF(final List<Tag> tags) {
+ return ControlTagType.isAutoInvoicingOff(Collections2.transform(tags, new Function<Tag, UUID>() {
+ @Nullable
+ @Override
+ public UUID apply(@Nullable final Tag tag) {
+ return tag.getTagDefinitionId();
+ }
+ }));
+ }
+}
diff --git a/junction/src/test/java/org/killbill/billing/junction/glue/TestJunctionModule.java b/junction/src/test/java/org/killbill/billing/junction/glue/TestJunctionModule.java
new file mode 100644
index 0000000..9f84e7d
--- /dev/null
+++ b/junction/src/test/java/org/killbill/billing/junction/glue/TestJunctionModule.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.entitlement.api.svcs.DefaultInternalBlockingApi;
+import org.killbill.billing.entitlement.block.BlockingChecker;
+import org.killbill.billing.entitlement.block.MockBlockingChecker;
+import org.killbill.billing.entitlement.dao.BlockingStateDao;
+import org.killbill.billing.entitlement.dao.MockBlockingStateDao;
+import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.mock.glue.MockEntitlementModule;
+import org.killbill.billing.util.glue.CacheModule;
+import org.killbill.billing.util.glue.CallContextModule;
+import org.killbill.billing.util.glue.MetricsModule;
+
+public class TestJunctionModule extends DefaultJunctionModule {
+
+ public TestJunctionModule(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+
+ install(new MetricsModule());
+ install(new CacheModule(configSource));
+ install(new CallContextModule());
+ }
+
+ public class MockEntitlementModuleForJunction extends MockEntitlementModule {
+
+ @Override
+ public void installBlockingApi() {
+ bind(BlockingInternalApi.class).to(DefaultInternalBlockingApi.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installBlockingStateDao() {
+ bind(BlockingStateDao.class).to(MockBlockingStateDao.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installBlockingChecker() {
+ bind(BlockingChecker.class).to(MockBlockingChecker.class).asEagerSingleton();
+ }
+ }
+}
diff --git a/junction/src/test/java/org/killbill/billing/junction/glue/TestJunctionModuleNoDB.java b/junction/src/test/java/org/killbill/billing/junction/glue/TestJunctionModuleNoDB.java
new file mode 100644
index 0000000..ec3b0af
--- /dev/null
+++ b/junction/src/test/java/org/killbill/billing/junction/glue/TestJunctionModuleNoDB.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+import org.killbill.billing.catalog.MockCatalogModule;
+import org.killbill.billing.mock.glue.MockAccountModule;
+import org.killbill.billing.mock.glue.MockNonEntityDaoModule;
+import org.killbill.billing.mock.glue.MockSubscriptionModule;
+import org.killbill.billing.mock.glue.MockTagModule;
+import org.killbill.notificationq.MockNotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueConfig;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.util.bus.InMemoryBusModule;
+
+import com.google.common.collect.ImmutableMap;
+
+public class TestJunctionModuleNoDB extends TestJunctionModule {
+
+ public TestJunctionModuleNoDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+
+ install(new GuicyKillbillTestNoDBModule());
+ install(new MockNonEntityDaoModule());
+ install(new InMemoryBusModule(configSource));
+ install(new MockAccountModule());
+ install(new MockCatalogModule());
+ install(new MockSubscriptionModule());
+ install(new MockEntitlementModuleForJunction());
+ install(new MockTagModule());
+ installNotificationQueue();
+ }
+
+ private void installNotificationQueue() {
+ bind(NotificationQueueService.class).to(MockNotificationQueueService.class).asEagerSingleton();
+ configureNotificationQueueConfig();
+ }
+
+ protected void configureNotificationQueueConfig() {
+ final NotificationQueueConfig config = new ConfigurationObjectFactory(configSource).buildWithReplacements(NotificationQueueConfig.class,
+ ImmutableMap.<String, String>of("instanceName", "main"));
+ bind(NotificationQueueConfig.class).toInstance(config);
+ }
+}
diff --git a/junction/src/test/java/org/killbill/billing/junction/glue/TestJunctionModuleWithEmbeddedDB.java b/junction/src/test/java/org/killbill/billing/junction/glue/TestJunctionModuleWithEmbeddedDB.java
new file mode 100644
index 0000000..30583cd
--- /dev/null
+++ b/junction/src/test/java/org/killbill/billing/junction/glue/TestJunctionModuleWithEmbeddedDB.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
+import org.killbill.billing.account.glue.DefaultAccountModule;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.catalog.glue.CatalogModule;
+import org.killbill.billing.entitlement.glue.DefaultEntitlementModule;
+import org.killbill.billing.subscription.glue.DefaultSubscriptionModule;
+import org.killbill.billing.util.glue.BusModule;
+import org.killbill.billing.util.glue.MetricsModule;
+import org.killbill.billing.util.glue.NonEntityDaoModule;
+import org.killbill.billing.util.glue.NotificationQueueModule;
+import org.killbill.billing.util.glue.TagStoreModule;
+
+public class TestJunctionModuleWithEmbeddedDB extends TestJunctionModule {
+
+ public TestJunctionModuleWithEmbeddedDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+
+ install(new GuicyKillbillTestWithEmbeddedDBModule());
+ install(new NonEntityDaoModule());
+ install(new CatalogModule(configSource));
+ install(new DefaultAccountModule(configSource));
+ install(new DefaultEntitlementModule(configSource));
+ install(new NotificationQueueModule(configSource));
+ install(new DefaultSubscriptionModule(configSource));
+ install(new BusModule(configSource));
+ install(new MetricsModule());
+ install(new TagStoreModule());
+
+ bind(TestApiListener.class).asEagerSingleton();
+ }
+}
diff --git a/junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteNoDB.java b/junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteNoDB.java
new file mode 100644
index 0000000..f13d01e
--- /dev/null
+++ b/junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteNoDB.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.entitlement.dao.BlockingStateDao;
+import org.killbill.billing.junction.glue.TestJunctionModuleNoDB;
+import org.killbill.billing.junction.plumbing.billing.BillCycleDayCalculator;
+import org.killbill.billing.junction.plumbing.billing.BlockingCalculator;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.tag.dao.TagDao;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public abstract class JunctionTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ @Inject
+ protected AccountInternalApi accountInternalApi;
+ @Inject
+ protected BillCycleDayCalculator billCycleDayCalculator;
+ @Inject
+ protected BillingInternalApi billingInternalApi;
+ @Inject
+ protected BlockingCalculator blockingCalculator;
+ @Inject
+ protected CatalogService catalogService;
+ @Inject
+ protected SubscriptionBaseInternalApi subscriptionInternalApi;
+ @Inject
+ protected PersistentBus bus;
+ @Inject
+ protected TagDao tagDao;
+ @Inject
+ protected TagInternalApi tagInternalApi;
+ @Inject
+ protected BlockingStateDao blockingStateDao;
+
+ @BeforeClass(groups = "fast")
+ protected void beforeClass() throws Exception {
+ final Injector injector = Guice.createInjector(new TestJunctionModuleNoDB(configSource));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ bus.start();
+ }
+
+ @AfterMethod(groups = "fast")
+ public void afterMethod() {
+ bus.stop();
+ }
+}
diff --git a/junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteWithEmbeddedDB.java b/junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..7f24b1f
--- /dev/null
+++ b/junction/src/test/java/org/killbill/billing/junction/JunctionTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction;
+
+import java.net.URL;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.catalog.DefaultCatalogService;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.entitlement.DefaultEntitlementService;
+import org.killbill.billing.entitlement.EntitlementService;
+import org.killbill.billing.entitlement.api.EntitlementApi;
+import org.killbill.billing.junction.glue.TestJunctionModuleWithEmbeddedDB;
+import org.killbill.billing.mock.MockAccountBuilder;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseService;
+import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+public abstract class JunctionTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
+
+ protected static final Logger log = LoggerFactory.getLogger(JunctionTestSuiteWithEmbeddedDB.class);
+
+ @Inject
+ protected AccountUserApi accountApi;
+ @Inject
+ protected BlockingInternalApi blockingInternalApi;
+ @Inject
+ protected EntitlementApi entitlementApi;
+ @Inject
+ protected BillingInternalApi billingInternalApi;
+ @Inject
+ protected CatalogService catalogService;
+ @Inject
+ protected SubscriptionBaseInternalApi subscriptionInternalApi;
+ @Inject
+ protected PersistentBus bus;
+ @Inject
+ protected TestApiListener testListener;
+ @Inject
+ protected BusService busService;
+ @Inject
+ protected SubscriptionBaseService subscriptionBaseService;
+ @Inject
+ protected EntitlementService entitlementService;
+ @Inject
+ protected InternalCallContextFactory internalCallContextFactory;
+
+ protected Catalog catalog;
+
+ private void loadSystemPropertiesFromClasspath(final String resource) {
+ final URL url = JunctionTestSuiteWithEmbeddedDB.class.getResource(resource);
+ Assert.assertNotNull(url);
+
+ configSource.merge(url);
+ }
+
+ @BeforeClass(groups = "slow")
+ protected void beforeClass() throws Exception {
+ loadSystemPropertiesFromClasspath("/junction.properties");
+ final Injector injector = Guice.createInjector(Stage.PRODUCTION, new TestJunctionModuleWithEmbeddedDB(configSource));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ startTestFamework();
+ this.catalog = initCatalog(catalogService);
+
+ // Make sure we start with a clean state
+ assertListenerStatus();
+ }
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ // Make sure we finish in a clean state
+ assertListenerStatus();
+
+ stopTestFramework();
+ }
+
+ private Catalog initCatalog(final CatalogService catalogService) throws Exception {
+ ((DefaultCatalogService) catalogService).loadCatalog();
+ final Catalog catalog = catalogService.getFullCatalog();
+ assertNotNull(catalog);
+ return catalog;
+ }
+
+ private void startTestFamework() throws Exception {
+ log.debug("STARTING TEST FRAMEWORK");
+
+ resetTestListener(testListener);
+
+ resetClockToStartOfTest(clock);
+
+ startBusAndRegisterListener(busService, testListener);
+
+ restartSubscriptionService(subscriptionBaseService);
+ restartEntitlementService(entitlementService);
+
+ log.debug("STARTED TEST FRAMEWORK");
+ }
+
+ private void stopTestFramework() throws Exception {
+ log.debug("STOPPING TEST FRAMEWORK");
+ stopBusAndUnregisterListener(busService, testListener);
+ stopSubscriptionService(subscriptionBaseService);
+ stopEntitlementService(entitlementService);
+ log.debug("STOPPED TEST FRAMEWORK");
+ }
+
+ private void resetTestListener(final TestApiListener testListener) {
+ // RESET LIST OF EXPECTED EVENTS
+ if (testListener != null) {
+ testListener.reset();
+ }
+ }
+
+ private void resetClockToStartOfTest(final ClockMock clock) {
+ clock.resetDeltaFromReality();
+
+ // Date at which all tests start-- we create the date object here after the system properties which set the JVM in UTC have been set.
+ final DateTime testStartDate = new DateTime(2012, 5, 7, 0, 3, 42, 0);
+ clock.setDeltaFromReality(testStartDate.getMillis() - clock.getUTCNow().getMillis());
+ }
+
+ private void startBusAndRegisterListener(final BusService busService, final TestApiListener testListener) throws Exception {
+ busService.getBus().start();
+ busService.getBus().register(testListener);
+ }
+
+ private void restartSubscriptionService(final SubscriptionBaseService subscriptionBaseService) {
+ // START NOTIFICATION QUEUE FOR SUBSCRIPTION
+ ((DefaultSubscriptionBaseService) subscriptionBaseService).initialize();
+ ((DefaultSubscriptionBaseService) subscriptionBaseService).start();
+ }
+
+ private void restartEntitlementService(final EntitlementService entitlementService) {
+ // START NOTIFICATION QUEUE FOR ENTITLEMENT
+ ((DefaultEntitlementService) entitlementService).initialize();
+ ((DefaultEntitlementService) entitlementService).start();
+ }
+
+ private void stopBusAndUnregisterListener(final BusService busService, final TestApiListener testListener) throws Exception {
+ busService.getBus().unregister(testListener);
+ busService.getBus().stop();
+ }
+
+ private void stopSubscriptionService(final SubscriptionBaseService subscriptionBaseService) throws Exception {
+ ((DefaultSubscriptionBaseService) subscriptionBaseService).stop();
+ }
+
+ private void stopEntitlementService(final EntitlementService entitlementService) throws Exception {
+ ((DefaultEntitlementService) entitlementService).stop();
+ }
+
+ protected AccountData getAccountData(final int billingDay) {
+ return new MockAccountBuilder().name(UUID.randomUUID().toString().substring(1, 8))
+ .firstNameLength(6)
+ .email(UUID.randomUUID().toString().substring(1, 8))
+ .phone(UUID.randomUUID().toString().substring(1, 8))
+ .migrated(false)
+ .isNotifiedForInvoices(false)
+ .externalKey(UUID.randomUUID().toString().substring(1, 8))
+ .billingCycleDayLocal(billingDay)
+ .currency(Currency.USD)
+ .paymentMethodId(UUID.randomUUID())
+ .timeZone(DateTimeZone.UTC)
+ .build();
+ }
+
+ protected void assertListenerStatus() {
+ testListener.assertListenerStatus();
+ }
+}
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java
new file mode 100644
index 0000000..674aae2
--- /dev/null
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillCycleDayCalculator.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.plumbing.billing;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.catalog.api.BillingAlignment;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.junction.JunctionTestSuiteNoDB;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+
+public class TestBillCycleDayCalculator extends JunctionTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testCalculateBCDForAOWithBPCancelledBundleAligned() throws Exception {
+ final DateTimeZone accountTimeZone = DateTimeZone.UTC;
+ final DateTime bpStartDateUTC = new DateTime(2012, 7, 16, 21, 0, 0, DateTimeZone.UTC);
+ final int expectedBCDUTC = 16;
+
+ // Create a Bundle associated with a subscription
+ final SubscriptionBaseBundle bundle = Mockito.mock(SubscriptionBaseBundle.class);
+ final SubscriptionBase subscription = Mockito.mock(SubscriptionBase.class);
+ Mockito.when(subscription.getStartDate()).thenReturn(bpStartDateUTC);
+
+ // subscription.getCurrentPlan() will return null as expected (cancelled BP)
+ Mockito.when(subscriptionInternalApi.getBaseSubscription(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(subscription);
+
+ // Create a the base plan associated with that subscription
+ final Plan plan = Mockito.mock(Plan.class);
+ Mockito.when(plan.dateOfFirstRecurringNonZeroCharge(bpStartDateUTC, null)).thenReturn(bpStartDateUTC);
+ final Catalog catalog = Mockito.mock(Catalog.class);
+ Mockito.when(catalog.findPlan(Mockito.anyString(), Mockito.<DateTime>any(), Mockito.<DateTime>any())).thenReturn(plan);
+ Mockito.when(subscription.getLastActivePlan()).thenReturn(plan);
+
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getTimeZone()).thenReturn(accountTimeZone);
+ final Integer billCycleDayLocal = billCycleDayCalculator.calculateBcdForAlignment(BillingAlignment.BUNDLE, bundle, subscription,
+ account, catalog, null, internalCallContext);
+
+ Assert.assertEquals(billCycleDayLocal, (Integer) expectedBCDUTC);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateBCDWithTimeZoneHST() throws Exception {
+ final DateTimeZone accountTimeZone = DateTimeZone.forID("HST");
+ final DateTime startDateUTC = new DateTime("2012-07-16T21:17:03.000Z", DateTimeZone.UTC);
+ final int bcdLocal = 16;
+
+ verifyBCDCalculation(accountTimeZone, startDateUTC, bcdLocal);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateBCDWithTimeZoneCEST() throws Exception {
+ final DateTimeZone accountTimeZone = DateTimeZone.forID("Europe/Paris");
+ final DateTime startDateUTC = new DateTime("2012-07-16T21:17:03.000Z", DateTimeZone.UTC);
+ final int bcdLocal = 16;
+
+ verifyBCDCalculation(accountTimeZone, startDateUTC, bcdLocal);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateBCDWithTimeZoneUTC() throws Exception {
+ final DateTimeZone accountTimeZone = DateTimeZone.UTC;
+ final DateTime startDateUTC = new DateTime("2012-07-16T21:17:03.000Z", DateTimeZone.UTC);
+ final int bcdLocal = 16;
+
+ verifyBCDCalculation(accountTimeZone, startDateUTC, bcdLocal);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateBCDWithTimeZoneEEST() throws Exception {
+ final DateTimeZone accountTimeZone = DateTimeZone.forID("+0300");
+ final DateTime startDateUTC = new DateTime("2012-07-16T21:17:03.000Z", DateTimeZone.UTC);
+ final int bcdLocal = 17;
+
+ verifyBCDCalculation(accountTimeZone, startDateUTC, bcdLocal);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateBCDWithTimeZoneJST() throws Exception {
+ final DateTimeZone accountTimeZone = DateTimeZone.forID("Asia/Tokyo");
+ final DateTime startDateUTC = new DateTime("2012-07-16T21:17:03.000Z", DateTimeZone.UTC);
+ final int bcdLocal = 17;
+
+ verifyBCDCalculation(accountTimeZone, startDateUTC, bcdLocal);
+ }
+
+ @Test(groups = "fast")
+ public void testCalculateBCDWithSubscriptionDateNotInUTC() throws Exception {
+ // Test to verify the computations don't rely implicitly on UTC
+ final DateTimeZone accountTimeZone = DateTimeZone.forID("Asia/Tokyo");
+ final DateTime startDate = new DateTime("2012-07-16T21:17:03.000Z", DateTimeZone.forID("HST"));
+ final int bcdLocal = 17;
+
+ verifyBCDCalculation(accountTimeZone, startDate, bcdLocal);
+ }
+
+ private void verifyBCDCalculation(final DateTimeZone accountTimeZone, final DateTime startDateUTC, final int bcdLocal) throws AccountApiException, CatalogApiException {
+ final BillCycleDayCalculator billCycleDayCalculator = new BillCycleDayCalculator(Mockito.mock(CatalogService.class), Mockito.mock(SubscriptionBaseInternalApi.class));
+
+ final SubscriptionBase subscription = Mockito.mock(SubscriptionBase.class);
+ Mockito.when(subscription.getStartDate()).thenReturn(startDateUTC);
+
+ final Plan plan = Mockito.mock(Plan.class);
+ Mockito.when(plan.dateOfFirstRecurringNonZeroCharge(startDateUTC, null)).thenReturn(startDateUTC);
+
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getTimeZone()).thenReturn(accountTimeZone);
+
+ final Integer bcd = billCycleDayCalculator.calculateBcdFromSubscription(subscription, plan, account, Mockito.mock(Catalog.class), internalCallContext);
+ Assert.assertEquals(bcd, (Integer) bcdLocal);
+ }
+}
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
new file mode 100644
index 0000000..9d677a4
--- /dev/null
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.plumbing.billing;
+
+import com.google.common.collect.ImmutableList;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.catalog.MockCatalog;
+import org.killbill.billing.catalog.api.BillingAlignment;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.InternationalPrice;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.entitlement.dao.MockBlockingStateDao;
+import org.killbill.billing.junction.JunctionTestSuiteNoDB;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.mock.MockEffectiveSubscriptionEvent;
+import org.killbill.billing.mock.MockSubscription;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingEventSet;
+import org.killbill.billing.junction.BillingModeType;
+import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.dao.MockTagDao;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.UUID;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+public class TestBillingApi extends JunctionTestSuiteNoDB {
+
+ private static final String DISABLED_BUNDLE = "disabled-bundle";
+ private static final String CLEAR_BUNDLE = "clear-bundle";
+
+ private static final UUID eventId = new UUID(0L, 0L);
+ private static final UUID subId = new UUID(1L, 0L);
+ private static final UUID bunId = new UUID(2L, 0L);
+
+ private List<EffectiveSubscriptionInternalEvent> effectiveSubscriptionTransitions;
+ private SubscriptionBase subscription;
+ private MockCatalog catalog;
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ final SubscriptionBaseBundle bundle = Mockito.mock(SubscriptionBaseBundle.class);
+ Mockito.when(bundle.getId()).thenReturn(bunId);
+ final List<SubscriptionBaseBundle> bundles = ImmutableList.<SubscriptionBaseBundle>of(bundle);
+
+ effectiveSubscriptionTransitions = new LinkedList<EffectiveSubscriptionInternalEvent>();
+
+ final DateTime subscriptionStartDate = clock.getUTCNow().minusDays(3);
+ subscription = new MockSubscription(subId, bunId, null, subscriptionStartDate, effectiveSubscriptionTransitions);
+ final List<SubscriptionBase> subscriptions = ImmutableList.<SubscriptionBase>of(subscription);
+
+ Mockito.when(subscriptionInternalApi.getBundlesForAccount(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(bundles);
+ Mockito.when(subscriptionInternalApi.getSubscriptionsForBundle(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(subscriptions);
+ Mockito.when(subscriptionInternalApi.getSubscriptionFromId(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(subscription);
+ Mockito.when(subscriptionInternalApi.getBundleFromId(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(bundle);
+ Mockito.when(subscriptionInternalApi.getBaseSubscription(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(subscription);
+ Mockito.when(subscriptionInternalApi.getBillingTransitions(Mockito.<SubscriptionBase>any(), Mockito.<InternalTenantContext>any())).thenReturn(effectiveSubscriptionTransitions);
+ Mockito.when(subscriptionInternalApi.getAllTransitions(Mockito.<SubscriptionBase>any(), Mockito.<InternalTenantContext>any())).thenReturn(effectiveSubscriptionTransitions);
+
+ catalog = ((MockCatalog) catalogService.getCurrentCatalog());
+ // TODO The MockCatalog module returns two different things for full vs current catalog
+ Mockito.when(catalogService.getFullCatalog()).thenReturn(catalog);
+ // Set a default alignment
+ catalog.setBillingAlignment(BillingAlignment.ACCOUNT);
+
+ // Cleanup mock daos
+ ((MockBlockingStateDao) blockingStateDao).clear();
+ ((MockTagDao) tagDao).clear();
+ }
+
+ @Test(groups = "fast")
+ public void testBillingEventsEmpty() throws AccountApiException {
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(new UUID(0L, 0L), internalCallContext);
+ Assert.assertEquals(events.size(), 0);
+ }
+
+ @Test(groups = "fast")
+ public void testBillingEventsNoBillingPeriod() throws CatalogApiException, AccountApiException {
+ final Plan nextPlan = catalog.findPlan("PickupTrialEvergreen10USD", clock.getUTCNow());
+ // The trial has no billing period
+ final PlanPhase nextPhase = nextPlan.getAllPhases()[0];
+ final DateTime now = createSubscriptionCreationEvent(nextPlan, nextPhase);
+
+ final Account account = createAccount(10);
+
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ checkFirstEvent(events, nextPlan, account.getBillCycleDayLocal(), subId, now, nextPhase, SubscriptionBaseTransitionType.CREATE.toString());
+ }
+
+ @Test(groups = "fast")
+ public void testBillingEventsSubscriptionAligned() throws CatalogApiException, AccountApiException {
+ final Plan nextPlan = catalog.findPlan("PickupTrialEvergreen10USD", clock.getUTCNow());
+ final PlanPhase nextPhase = nextPlan.getAllPhases()[1];
+ final DateTime now = createSubscriptionCreationEvent(nextPlan, nextPhase);
+
+ final Account account = createAccount(1);
+
+ catalog.setBillingAlignment(BillingAlignment.SUBSCRIPTION);
+
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ // The expected BCD is when the subscription started since we skip the trial phase
+ checkFirstEvent(events, nextPlan, subscription.getStartDate().getDayOfMonth(), subId, now, nextPhase, SubscriptionBaseTransitionType.CREATE.toString());
+ }
+
+ @Test(groups = "fast")
+ public void testBillingEventsAccountAligned() throws CatalogApiException, AccountApiException {
+ final Plan nextPlan = catalog.findPlan("PickupTrialEvergreen10USD", clock.getUTCNow());
+ final PlanPhase nextPhase = nextPlan.getAllPhases()[1];
+ final DateTime now = createSubscriptionCreationEvent(nextPlan, nextPhase);
+
+ final Account account = createAccount(32);
+
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ // The expected BCD is the account BCD (account aligned by default)
+ checkFirstEvent(events, nextPlan, 32, subId, now, nextPhase, SubscriptionBaseTransitionType.CREATE.toString());
+ }
+
+ @Test(groups = "fast")
+ public void testBillingEventsBundleAligned() throws CatalogApiException, AccountApiException {
+ final Plan nextPlan = catalog.findPlan("Horn1USD", clock.getUTCNow());
+ final PlanPhase nextPhase = nextPlan.getAllPhases()[0];
+ final DateTime now = createSubscriptionCreationEvent(nextPlan, nextPhase);
+
+ final Account account = createAccount(1);
+
+ catalog.setBillingAlignment(BillingAlignment.BUNDLE);
+ ((MockSubscription) subscription).setPlan(catalog.findPlan("PickupTrialEvergreen10USD", now));
+
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+ // The expected BCD is when the subscription started
+ checkFirstEvent(events, nextPlan, subscription.getStartDate().getDayOfMonth(), subId, now, nextPhase, SubscriptionBaseTransitionType.CREATE.toString());
+ }
+
+ @Test(groups = "fast")
+ public void testBillingEventsWithBlock() throws CatalogApiException, AccountApiException {
+ final Plan nextPlan = catalog.findPlan("PickupTrialEvergreen10USD", clock.getUTCNow());
+ final PlanPhase nextPhase = nextPlan.getAllPhases()[1];
+ final DateTime now = createSubscriptionCreationEvent(nextPlan, nextPhase);
+
+ final Account account = createAccount(32);
+
+ blockingStateDao.setBlockingState(new DefaultBlockingState(bunId, BlockingStateType.SUBSCRIPTION_BUNDLE, DISABLED_BUNDLE, "test", true, true, true, now.plusDays(1)), clock, internalCallContext);
+ blockingStateDao.setBlockingState(new DefaultBlockingState(bunId, BlockingStateType.SUBSCRIPTION_BUNDLE, CLEAR_BUNDLE, "test", false, false, false, now.plusDays(2)), clock, internalCallContext);
+
+ final SortedSet<BillingEvent> events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+
+ Assert.assertEquals(events.size(), 3);
+ final Iterator<BillingEvent> it = events.iterator();
+
+ checkEvent(it.next(), nextPlan, account.getBillCycleDayLocal(), subId, now, nextPhase, SubscriptionBaseTransitionType.CREATE.toString(), nextPhase.getFixedPrice(), nextPhase.getRecurringPrice());
+ checkEvent(it.next(), nextPlan, account.getBillCycleDayLocal(), subId, now.plusDays(1), nextPhase, SubscriptionBaseTransitionType.START_BILLING_DISABLED.toString(), null, null);
+ checkEvent(it.next(), nextPlan, account.getBillCycleDayLocal(), subId, now.plusDays(2), nextPhase, SubscriptionBaseTransitionType.END_BILLING_DISABLED.toString(), nextPhase.getFixedPrice(), nextPhase.getRecurringPrice());
+ }
+
+ @Test(groups = "fast")
+ public void testBillingEventsAutoInvoicingOffAccount() throws CatalogApiException, AccountApiException, TagApiException {
+ final Plan nextPlan = catalog.findPlan("PickupTrialEvergreen10USD", clock.getUTCNow());
+ final PlanPhase nextPhase = nextPlan.getAllPhases()[1];
+ createSubscriptionCreationEvent(nextPlan, nextPhase);
+
+ final Account account = createAccount(32);
+
+ tagInternalApi.addTag(account.getId(), ObjectType.ACCOUNT, ControlTagType.AUTO_INVOICING_OFF.getId(), internalCallContext);
+
+ final BillingEventSet events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+
+ assertEquals(events.isAccountAutoInvoiceOff(), true);
+ assertEquals(events.size(), 0);
+ }
+
+ @Test(groups = "fast")
+ public void testBillingEventsAutoInvoicingOffBundle() throws CatalogApiException, AccountApiException, TagApiException {
+ final Plan nextPlan = catalog.findPlan("PickupTrialEvergreen10USD", clock.getUTCNow());
+ final PlanPhase nextPhase = nextPlan.getAllPhases()[1];
+ createSubscriptionCreationEvent(nextPlan, nextPhase);
+
+ final Account account = createAccount(32);
+
+ tagInternalApi.addTag(bunId, ObjectType.BUNDLE, ControlTagType.AUTO_INVOICING_OFF.getId(), internalCallContext);
+
+ final BillingEventSet events = billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext);
+
+ assertEquals(events.getSubscriptionIdsWithAutoInvoiceOff().size(), 1);
+ assertEquals(events.getSubscriptionIdsWithAutoInvoiceOff().get(0), subId);
+ assertEquals(events.size(), 0);
+ }
+
+ private void checkFirstEvent(final SortedSet<BillingEvent> events, final Plan nextPlan,
+ final int BCD, final UUID id, final DateTime time, final PlanPhase nextPhase, final String desc) throws CatalogApiException {
+ Assert.assertEquals(events.size(), 1);
+ checkEvent(events.first(), nextPlan, BCD, id, time, nextPhase, desc, nextPhase.getFixedPrice(), nextPhase.getRecurringPrice());
+ }
+
+ private void checkEvent(final BillingEvent event, final Plan nextPlan, final int BCD, final UUID id, final DateTime time,
+ final PlanPhase nextPhase, final String desc, final InternationalPrice fixedPrice, final InternationalPrice recurringPrice) throws CatalogApiException {
+ if (fixedPrice != null) {
+ Assert.assertEquals(fixedPrice.getPrice(Currency.USD), event.getFixedPrice());
+ } else {
+ assertNull(event.getFixedPrice());
+ }
+
+ if (recurringPrice != null) {
+ Assert.assertEquals(recurringPrice.getPrice(Currency.USD), event.getRecurringPrice());
+ } else {
+ assertNull(event.getRecurringPrice());
+ }
+
+ Assert.assertEquals(BCD, event.getBillCycleDayLocal());
+ Assert.assertEquals(id, event.getSubscription().getId());
+ Assert.assertEquals(time.getDayOfMonth(), event.getEffectiveDate().getDayOfMonth());
+ Assert.assertEquals(nextPhase, event.getPlanPhase());
+ Assert.assertEquals(nextPlan, event.getPlan());
+ if (!SubscriptionBaseTransitionType.START_BILLING_DISABLED.equals(event.getTransitionType())) {
+ Assert.assertEquals(nextPhase.getBillingPeriod(), event.getBillingPeriod());
+ }
+ Assert.assertEquals(BillingModeType.IN_ADVANCE, event.getBillingMode());
+ Assert.assertEquals(desc, event.getTransitionType().toString());
+ }
+
+ private Account createAccount(final int billCycleDay) throws AccountApiException {
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getBillCycleDayLocal()).thenReturn(billCycleDay);
+ Mockito.when(account.getCurrency()).thenReturn(Currency.USD);
+ Mockito.when(account.getId()).thenReturn(UUID.randomUUID());
+ Mockito.when(account.getTimeZone()).thenReturn(DateTimeZone.UTC);
+ Mockito.when(accountInternalApi.getAccountById(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(account);
+ return account;
+ }
+
+ private DateTime createSubscriptionCreationEvent(final Plan nextPlan, final PlanPhase nextPhase) throws CatalogApiException {
+ final DateTime now = clock.getUTCNow();
+ final DateTime then = now.minusDays(1);
+ final PriceList nextPriceList = catalog.findPriceList(PriceListSet.DEFAULT_PRICELIST_NAME, now);
+
+ final EffectiveSubscriptionInternalEvent t = new MockEffectiveSubscriptionEvent(
+ eventId, subId, bunId, then, now, null, null, null, null, EntitlementState.ACTIVE,
+ nextPlan.getName(), nextPhase.getName(),
+ nextPriceList.getName(), 1L,
+ SubscriptionBaseTransitionType.CREATE, 1, null, 1L, 2L, null);
+
+ effectiveSubscriptionTransitions.add(t);
+ return now;
+ }
+}
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultBillingEvent.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultBillingEvent.java
new file mode 100644
index 0000000..cb25cc1
--- /dev/null
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultBillingEvent.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.plumbing.billing;
+
+import java.math.BigDecimal;
+import java.util.Iterator;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.DefaultPrice;
+import org.killbill.billing.catalog.MockInternationalPrice;
+import org.killbill.billing.catalog.MockPlan;
+import org.killbill.billing.catalog.MockPlanPhase;
+import org.killbill.billing.catalog.api.BillingPeriod;
+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.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.junction.JunctionTestSuiteNoDB;
+import org.killbill.billing.mock.MockAccountBuilder;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.BillingModeType;
+
+public class TestDefaultBillingEvent extends JunctionTestSuiteNoDB {
+
+ private static final UUID ID_ZERO = new UUID(0L, 0L);
+ private static final UUID ID_ONE = new UUID(0L, 1L);
+ private static final UUID ID_TWO = new UUID(0L, 2L);
+
+ @Test(groups = "fast")
+ public void testEntitlementEventsHappeningAtTheSameTimeAsOverdueEvents() throws Exception {
+ final BillingEvent event0 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-31T00:02:04.000Z"), SubscriptionBaseTransitionType.START_BILLING_DISABLED);
+ final BillingEvent event1 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-31T00:02:04.000Z"), SubscriptionBaseTransitionType.CREATE);
+ final BillingEvent event2 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-31T00:02:05.000Z"), SubscriptionBaseTransitionType.CHANGE);
+ final BillingEvent event3 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-31T00:02:05.000Z"), SubscriptionBaseTransitionType.END_BILLING_DISABLED);
+
+ final SortedSet<BillingEvent> set = new TreeSet<BillingEvent>();
+ set.add(event0);
+ set.add(event1);
+ set.add(event2);
+ set.add(event3);
+
+ final Iterator<BillingEvent> it = set.iterator();
+
+ Assert.assertEquals(event1, it.next());
+ Assert.assertEquals(event0, it.next());
+ Assert.assertEquals(event3, it.next());
+ Assert.assertEquals(event2, it.next());
+ }
+
+ @Test(groups = "fast")
+ public void testEdgeCaseAllEventsHappenAtTheSameTime() throws Exception {
+ final BillingEvent event0 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-31T00:02:04.000Z"), SubscriptionBaseTransitionType.START_BILLING_DISABLED);
+ final BillingEvent event1 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-31T00:02:04.000Z"), SubscriptionBaseTransitionType.CREATE, 1);
+ final BillingEvent event2 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-31T00:02:04.000Z"), SubscriptionBaseTransitionType.CHANGE, 2);
+ // Note the time delta here. Having a blocking duration of zero and events at the same time won't work as the backing tree set does local
+ // comparisons (and not global), making the END_BILLING_DISABLED start the first one in the set
+ final BillingEvent event3 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-31T00:02:05.000Z"), SubscriptionBaseTransitionType.END_BILLING_DISABLED);
+
+ final SortedSet<BillingEvent> set = new TreeSet<BillingEvent>();
+ set.add(event0);
+ set.add(event1);
+ set.add(event2);
+ set.add(event3);
+
+ final Iterator<BillingEvent> it = set.iterator();
+
+ Assert.assertEquals(event1, it.next());
+ Assert.assertEquals(event2, it.next());
+ Assert.assertEquals(event0, it.next());
+ Assert.assertEquals(event3, it.next());
+ }
+
+ @Test(groups = "fast")
+ public void testEventOrderingSubscription() {
+ final BillingEvent event0 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-31T00:02:04.000Z"), SubscriptionBaseTransitionType.CREATE);
+ final BillingEvent event1 = createEvent(subscription(ID_ONE), new DateTime("2012-01-31T00:02:04.000Z"), SubscriptionBaseTransitionType.CREATE);
+ final BillingEvent event2 = createEvent(subscription(ID_TWO), new DateTime("2012-01-31T00:02:04.000Z"), SubscriptionBaseTransitionType.CREATE);
+
+ final SortedSet<BillingEvent> set = new TreeSet<BillingEvent>();
+ set.add(event2);
+ set.add(event1);
+ set.add(event0);
+
+ final Iterator<BillingEvent> it = set.iterator();
+
+ Assert.assertEquals(event0, it.next());
+ Assert.assertEquals(event1, it.next());
+ Assert.assertEquals(event2, it.next());
+ }
+
+ @Test(groups = "fast")
+ public void testEventOrderingDate() {
+ final BillingEvent event0 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-01T00:02:04.000Z"), SubscriptionBaseTransitionType.CREATE);
+ final BillingEvent event1 = createEvent(subscription(ID_ZERO), new DateTime("2012-02-01T00:02:04.000Z"), SubscriptionBaseTransitionType.CREATE);
+ final BillingEvent event2 = createEvent(subscription(ID_ZERO), new DateTime("2012-03-01T00:02:04.000Z"), SubscriptionBaseTransitionType.CREATE);
+
+ final SortedSet<BillingEvent> set = new TreeSet<BillingEvent>();
+ set.add(event2);
+ set.add(event1);
+ set.add(event0);
+
+ final Iterator<BillingEvent> it = set.iterator();
+
+ Assert.assertEquals(event0, it.next());
+ Assert.assertEquals(event1, it.next());
+ Assert.assertEquals(event2, it.next());
+ }
+
+ @Test(groups = "fast")
+ public void testEventTotalOrdering() {
+ final BillingEvent event0 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-01T00:02:04.000Z"), SubscriptionBaseTransitionType.CREATE, 1L);
+ final BillingEvent event1 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-01T00:02:04.000Z"), SubscriptionBaseTransitionType.CANCEL, 2L);
+ final BillingEvent event2 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-01T00:02:04.000Z"), SubscriptionBaseTransitionType.RE_CREATE, 3L);
+
+ final SortedSet<BillingEvent> set = new TreeSet<BillingEvent>();
+ set.add(event2);
+ set.add(event1);
+ set.add(event0);
+
+ final Iterator<BillingEvent> it = set.iterator();
+
+ Assert.assertEquals(event0, it.next());
+ Assert.assertEquals(event1, it.next());
+ Assert.assertEquals(event2, it.next());
+ }
+
+ @Test(groups = "fast")
+ public void testEventOrderingMix() {
+ final BillingEvent event0 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-01T00:02:04.000Z"), SubscriptionBaseTransitionType.CREATE);
+ final BillingEvent event1 = createEvent(subscription(ID_ZERO), new DateTime("2012-01-02T00:02:04.000Z"), SubscriptionBaseTransitionType.CHANGE);
+ final BillingEvent event2 = createEvent(subscription(ID_ONE), new DateTime("2012-01-01T00:02:04.000Z"), SubscriptionBaseTransitionType.CANCEL);
+
+ final SortedSet<BillingEvent> set = new TreeSet<BillingEvent>();
+ set.add(event2);
+ set.add(event1);
+ set.add(event0);
+
+ final Iterator<BillingEvent> it = set.iterator();
+
+ Assert.assertEquals(event0, it.next());
+ Assert.assertEquals(event1, it.next());
+ Assert.assertEquals(event2, it.next());
+ }
+
+ @Test(groups = "fast")
+ public void testToString() throws Exception {
+ // Simple test to ensure we have an easy to read toString representation
+ final BillingEvent event = createEvent(subscription(ID_ZERO), new DateTime("2012-01-01T00:02:04.000Z", DateTimeZone.UTC), SubscriptionBaseTransitionType.CREATE);
+ Assert.assertEquals(event.toString(), "DefaultBillingEvent{type=CREATE, effectiveDate=2012-01-01T00:02:04.000Z, planPhaseName=Test-trial, subscriptionId=00000000-0000-0000-0000-000000000000, totalOrdering=1, accountId=" + event.getAccount().getId().toString() + "}");
+ }
+
+ private BillingEvent createEvent(final SubscriptionBase sub, final DateTime effectiveDate, final SubscriptionBaseTransitionType type) {
+ return createEvent(sub, effectiveDate, type, 1L);
+ }
+
+ private BillingEvent createEvent(final SubscriptionBase sub, final DateTime effectiveDate, final SubscriptionBaseTransitionType type, final long totalOrdering) {
+ final int billCycleDay = 1;
+
+ final Plan shotgun = new MockPlan();
+ final PlanPhase shotgunMonthly = createMockMonthlyPlanPhase(null, BigDecimal.ZERO, PhaseType.TRIAL);
+
+ final Account account = new MockAccountBuilder().build();
+ return new DefaultBillingEvent(account, sub, effectiveDate,
+ shotgun, shotgunMonthly,
+ BigDecimal.ZERO, null, Currency.USD, BillingPeriod.NO_BILLING_PERIOD, billCycleDay,
+ BillingModeType.IN_ADVANCE, "Test Event 1", totalOrdering, type, DateTimeZone.UTC);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate,
+ final BigDecimal fixedRate, final PhaseType phaseType) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ new MockInternationalPrice(new DefaultPrice(fixedRate, Currency.USD)),
+ BillingPeriod.MONTHLY, phaseType);
+ }
+
+ private SubscriptionBase subscription(final UUID id) {
+ final SubscriptionBase subscription = Mockito.mock(SubscriptionBase.class);
+ Mockito.when(subscription.getId()).thenReturn(id);
+ return subscription;
+ }
+}
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java
new file mode 100644
index 0000000..4b30b27
--- /dev/null
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestDefaultInternalBillingApi.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.junction.plumbing.billing;
+
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.BillingPeriod;
+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.EntitlementService;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.DefaultEntitlementApi;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.junction.JunctionTestSuiteWithEmbeddedDB;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultInternalBillingApi extends JunctionTestSuiteWithEmbeddedDB {
+
+ // See https://github.com/killbill/killbill/issues/123
+ //
+ // Pierre, why do we have invocationCount > 0 here?
+ //
+ // This test will exercise ProxyBlockingStateDao#BLOCKING_STATE_ORDERING_WITH_TIES_UNHANDLED - unfortunately, for some reason,
+ // the ordering doesn't seem deterministic. In some scenarii,
+ // BlockingState(idA, effectiveDate1, BLOCK), BlockingState(idA, effectiveDate2, CLEAR), BlockingState(idB, effectiveDate2, BLOCK), BlockingState(idB, effectiveDate3, CLEAR)
+ // is ordered
+ // BlockingState(idA, effectiveDate1, BLOCK), BlockingState(idB, effectiveDate2, BLOCK), BlockingState(idA, effectiveDate2, CLEAR), BlockingState(idB, effectiveDate3, CLEAR)
+ // The code BlockingCalculator#createBlockingDurations has been updated to support it, but we still want to make sure it actually works in both scenarii
+ // (invocationCount = 10 will trigger both usecases in my testing).
+ @Test(groups = "slow", description = "Check blocking states with same effective date are correctly handled", invocationCount = 10)
+ public void testBlockingStatesWithSameEffectiveDate() throws Exception {
+ final LocalDate initialDate = new LocalDate(2013, 8, 7);
+ clock.setDay(initialDate);
+
+ final Account account = accountApi.createAccount(getAccountData(7), callContext);
+ final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+
+ testListener.pushExpectedEvent(NextEvent.CREATE);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), initialDate, callContext);
+ final SubscriptionBase subscription = subscriptionInternalApi.getSubscriptionFromId(entitlement.getId(), internalCallContext);
+ assertListenerStatus();
+
+ final DateTime block1Date = clock.getUTCNow();
+ testListener.pushExpectedEvents(NextEvent.BLOCK, NextEvent.BLOCK);
+ final DefaultBlockingState state1 = new DefaultBlockingState(account.getId(),
+ BlockingStateType.ACCOUNT,
+ DefaultEntitlementApi.ENT_STATE_BLOCKED,
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ true,
+ true,
+ true,
+ block1Date);
+ blockingInternalApi.setBlockingState(state1, internalCallContext);
+ // Same date, we'll order by record id asc
+ final DefaultBlockingState state2 = new DefaultBlockingState(account.getId(),
+ BlockingStateType.ACCOUNT,
+ DefaultEntitlementApi.ENT_STATE_CLEAR,
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ false,
+ false,
+ false,
+ block1Date);
+ blockingInternalApi.setBlockingState(state2, internalCallContext);
+ assertListenerStatus();
+
+ clock.addDays(5);
+
+ final DateTime block2Date = clock.getUTCNow();
+ testListener.pushExpectedEvents(NextEvent.BLOCK, NextEvent.BLOCK);
+ final DefaultBlockingState state3 = new DefaultBlockingState(entitlement.getBundleId(),
+ BlockingStateType.SUBSCRIPTION_BUNDLE,
+ DefaultEntitlementApi.ENT_STATE_BLOCKED,
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ true,
+ true,
+ true,
+ block2Date);
+ blockingInternalApi.setBlockingState(state3, internalCallContext);
+ // Same date, we'll order by record id asc
+ final DefaultBlockingState state4 = new DefaultBlockingState(entitlement.getBundleId(),
+ BlockingStateType.SUBSCRIPTION_BUNDLE,
+ DefaultEntitlementApi.ENT_STATE_CLEAR,
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ false,
+ false,
+ false,
+ block2Date);
+ blockingInternalApi.setBlockingState(state4, internalCallContext);
+ assertListenerStatus();
+
+ final DateTime block3Date = block2Date.plusDays(3);
+
+ // Pass the phase
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.addDays(50);
+ assertListenerStatus();
+
+ final DateTime block4Date = clock.getUTCNow();
+ final DateTime block5Date = block4Date.plusDays(3);
+ // Only one event on the bus (for state5)
+ testListener.pushExpectedEvents(NextEvent.BLOCK);
+ // Insert the clear state first, to make sure the order in which we insert blocking states doesn't matter
+ // Since we are already in an ENT_STATE_CLEAR state for service ENTITLEMENT_SERVICE_NAME, we need to use a different
+ // state name to simulate this behavior (otherwise, by design, this event won't be created)
+ final DefaultBlockingState state6 = new DefaultBlockingState(entitlement.getBundleId(),
+ BlockingStateType.SUBSCRIPTION_BUNDLE,
+ DefaultEntitlementApi.ENT_STATE_CLEAR + "-something",
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ false,
+ false,
+ false,
+ block5Date);
+ blockingInternalApi.setBlockingState(state6, internalCallContext);
+ final DefaultBlockingState state5 = new DefaultBlockingState(entitlement.getBundleId(),
+ BlockingStateType.SUBSCRIPTION_BUNDLE,
+ DefaultEntitlementApi.ENT_STATE_BLOCKED + "-something",
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ true,
+ true,
+ true,
+ block4Date);
+ blockingInternalApi.setBlockingState(state5, internalCallContext);
+ assertListenerStatus();
+
+ // Now, add back blocking states at an earlier date, for a different blockable id, to make sure the effective
+ // date ordering is correctly respected when computing blocking durations
+ testListener.pushExpectedEvents(NextEvent.BLOCK, NextEvent.BLOCK);
+ final DefaultBlockingState state7 = new DefaultBlockingState(account.getId(),
+ BlockingStateType.ACCOUNT,
+ DefaultEntitlementApi.ENT_STATE_BLOCKED + "-something2",
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ true,
+ true,
+ true,
+ block3Date);
+ blockingInternalApi.setBlockingState(state7, internalCallContext);
+ final DefaultBlockingState state8 = new DefaultBlockingState(account.getId(),
+ BlockingStateType.ACCOUNT,
+ DefaultEntitlementApi.ENT_STATE_CLEAR + "-something2",
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ false,
+ false,
+ false,
+ block4Date);
+ blockingInternalApi.setBlockingState(state8, internalCallContext);
+ assertListenerStatus();
+
+ // Advance for state6 to be active
+ testListener.pushExpectedEvents(NextEvent.BLOCK);
+ clock.addDays(5);
+ assertListenerStatus();
+
+ // Expected blocking durations:
+ // * 2013-08-07 to 2013-08-07 (block1Date)
+ // * 2013-08-12 to 2013-08-12 (block2Date)
+ // * 2013-08-15 to 2013-10-04 [2013-08-15 to 2013-10-01 (block3Date -> block4Date) and 2013-10-01 to 2013-10-04 (block4Date -> block5Date)]
+ final List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext));
+ Assert.assertEquals(events.size(), 7);
+ Assert.assertEquals(events.get(0).getTransitionType(), SubscriptionBaseTransitionType.CREATE);
+ Assert.assertEquals(events.get(0).getEffectiveDate(), subscription.getStartDate());
+ Assert.assertEquals(events.get(1).getTransitionType(), SubscriptionBaseTransitionType.START_BILLING_DISABLED);
+ Assert.assertEquals(events.get(1).getEffectiveDate(), block1Date);
+ Assert.assertEquals(events.get(2).getTransitionType(), SubscriptionBaseTransitionType.END_BILLING_DISABLED);
+ Assert.assertEquals(events.get(2).getEffectiveDate(), block1Date);
+ Assert.assertEquals(events.get(3).getTransitionType(), SubscriptionBaseTransitionType.START_BILLING_DISABLED);
+ Assert.assertEquals(events.get(3).getEffectiveDate(), block2Date);
+ Assert.assertEquals(events.get(4).getTransitionType(), SubscriptionBaseTransitionType.END_BILLING_DISABLED);
+ Assert.assertEquals(events.get(4).getEffectiveDate(), block2Date);
+ Assert.assertEquals(events.get(5).getTransitionType(), SubscriptionBaseTransitionType.START_BILLING_DISABLED);
+ Assert.assertEquals(events.get(5).getEffectiveDate(), block3Date);
+ Assert.assertEquals(events.get(6).getTransitionType(), SubscriptionBaseTransitionType.END_BILLING_DISABLED);
+ Assert.assertEquals(events.get(6).getEffectiveDate(), block5Date);
+ }
+
+ // See https://github.com/killbill/killbill/commit/92042843e38a67f75495b207385e4c1f9ca60990#commitcomment-4749967
+ @Test(groups = "slow", description = "Check unblock then block states with same effective date are correctly handled", invocationCount = 10)
+ public void testUnblockThenBlockBlockingStatesWithSameEffectiveDate() throws Exception {
+ final LocalDate initialDate = new LocalDate(2013, 8, 7);
+ clock.setDay(initialDate);
+
+ final Account account = accountApi.createAccount(getAccountData(7), callContext);
+ final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+
+ testListener.pushExpectedEvent(NextEvent.CREATE);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), initialDate, callContext);
+ final SubscriptionBase subscription = subscriptionInternalApi.getSubscriptionFromId(entitlement.getId(), internalCallContext);
+ assertListenerStatus();
+
+ final DateTime block1Date = clock.getUTCNow();
+ testListener.pushExpectedEvents(NextEvent.BLOCK);
+ final DefaultBlockingState state1 = new DefaultBlockingState(account.getId(),
+ BlockingStateType.ACCOUNT,
+ DefaultEntitlementApi.ENT_STATE_BLOCKED,
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ true,
+ true,
+ true,
+ block1Date);
+ blockingInternalApi.setBlockingState(state1, internalCallContext);
+
+ clock.addDays(1);
+
+ final DateTime block2Date = clock.getUTCNow();
+ testListener.pushExpectedEvents(NextEvent.BLOCK, NextEvent.BLOCK);
+ final DefaultBlockingState state2 = new DefaultBlockingState(account.getId(),
+ BlockingStateType.ACCOUNT,
+ DefaultEntitlementApi.ENT_STATE_CLEAR,
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ false,
+ false,
+ false,
+ block2Date);
+ blockingInternalApi.setBlockingState(state2, internalCallContext);
+ // Same date
+ final DefaultBlockingState state3 = new DefaultBlockingState(account.getId(),
+ BlockingStateType.ACCOUNT,
+ DefaultEntitlementApi.ENT_STATE_BLOCKED,
+ EntitlementService.ENTITLEMENT_SERVICE_NAME,
+ true,
+ true,
+ true,
+ block2Date);
+ blockingInternalApi.setBlockingState(state3, internalCallContext);
+ assertListenerStatus();
+
+ // Nothing should happen
+ clock.addDays(3);
+ assertListenerStatus();
+
+ // Expected blocking duration:
+ // * 2013-08-07 to now [2013-08-07 to 2013-08-08 then 2013-08-08 to now]
+ final List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), internalCallContext));
+ Assert.assertEquals(events.size(), 2);
+ Assert.assertEquals(events.get(0).getTransitionType(), SubscriptionBaseTransitionType.CREATE);
+ Assert.assertEquals(events.get(0).getEffectiveDate(), subscription.getStartDate());
+ Assert.assertEquals(events.get(1).getTransitionType(), SubscriptionBaseTransitionType.START_BILLING_DISABLED);
+ Assert.assertEquals(events.get(1).getEffectiveDate(), block1Date);
+ }
+}
diff --git a/junction/src/test/resources/junction.properties b/junction/src/test/resources/junction.properties
index 87d47cb..11f0313 100644
--- a/junction/src/test/resources/junction.properties
+++ b/junction/src/test/resources/junction.properties
@@ -1,5 +1,5 @@
-killbill.catalog.uri=file:src/test/resources/catalog.xml
-killbill.billing.persistent.bus.main.sleep=100
-killbill.billing.persistent.bus.main.nbThreads=1
-killbill.billing.persistent.bus.main.claimed=1
+org.killbill.catalog.uri=file:src/test/resources/catalog.xml
+org.killbill.persistent.bus.main.sleep=100
+org.killbill.persistent.bus.main.nbThreads=1
+org.killbill.persistent.bus.main.claimed=1
user.timezone=UTC
NEWS 3(+3 -0)
diff --git a/NEWS b/NEWS
index 6b252fc..a3c72f4 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,6 @@
+0.9.1
+ Update packages com.ning -> org.killbill
+
0.8.13
Fix SQL query typo in unmarkPaymentMethodAsDeleted
osgi/pom.xml 64(+32 -32)
diff --git a/osgi/pom.xml b/osgi/pom.xml
index e98ab5e..7a18c47 100644
--- a/osgi/pom.xml
+++ b/osgi/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi</artifactId>
@@ -41,67 +41,67 @@
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.framework</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-osgi-bundles-lib-killbill</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-clock</artifactId>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-clock</artifactId>
- <type>test-jar</type>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-currency</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-notification</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-payment</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>javax.servlet</groupId>
- <artifactId>javax.servlet-api</artifactId>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.felix</groupId>
- <artifactId>org.apache.felix.framework</artifactId>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>org.apache.felix</groupId>
- <artifactId>org.osgi.core</artifactId>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-clock</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/api/DefaultOSGIUserApi.java b/osgi/src/main/java/org/killbill/billing/osgi/api/DefaultOSGIUserApi.java
new file mode 100644
index 0000000..42c8530
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/api/DefaultOSGIUserApi.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.api;
+
+public class DefaultOSGIUserApi implements OSGIUserApi {
+
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/ContextClassLoaderHelper.java b/osgi/src/main/java/org/killbill/billing/osgi/ContextClassLoaderHelper.java
new file mode 100644
index 0000000..61d704b
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/ContextClassLoaderHelper.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public class ContextClassLoaderHelper {
+
+
+ /*
+ http://impalablog.blogspot.com/2008/10/using-threads-callcontext-class-loader-in.html:
+
+ "Many existing java libraries are designed to run inside a container (J2EE container, Applet container etc).
+ Such containers explicitly define execution boundaries between the various components running within the container.
+ The container controls the execution boundaries and knows when a boundary is being crossed from one component to the next.
+
+ This level of boundary control allows a container to switch the callcontext of a thread when a component boundary is crossed.
+ Typically when a container detects a callcontext switch it will set the callcontext class loader on the thread to a class loader associated with the component which is being entered.
+ When the component is exited then the container will switch the callcontext class loader back to the previous callcontext class loader.
+
+ The OSGi Framework specification does not define what the callcontext class loader should be set to and does not define when it should be switched.
+ Part of the problem is the Framework is not always aware of when a component boundary is crossed."
+
+ => So our current implementation is to proxy all calls from Killbill to OSGI registered services, and set/unset classloader before/after entering the call
+
+ */
+
+ public static <T> T getWrappedServiceWithCorrectContextClassLoader(final T service) {
+
+ final Class<T> serviceClass = (Class<T>) service.getClass();
+ final List<Class> allServiceInterfaces = getAllInterfaces(serviceClass);
+ final Class[] serviceClassInterfaces = allServiceInterfaces.toArray(new Class[allServiceInterfaces.size()]);
+
+ final InvocationHandler handler = new InvocationHandler() {
+ @Override
+ public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
+ final ClassLoader initialContextClassLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(serviceClass.getClassLoader());
+ return method.invoke(service, args);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() != null) {
+ throw e.getCause();
+ } else {
+ throw new RuntimeException(e);
+ }
+ } finally {
+ Thread.currentThread().setContextClassLoader(initialContextClassLoader);
+ }
+ }
+ };
+ final T wrappedService = (T) Proxy.newProxyInstance(serviceClass.getClassLoader(),
+ serviceClassInterfaces,
+ handler);
+ return wrappedService;
+ }
+
+
+ // From apache-commons
+ private static List getAllInterfaces(Class cls) {
+ if (cls == null) {
+ return null;
+ }
+ List list = new ArrayList();
+ while (cls != null) {
+ Class[] interfaces = cls.getInterfaces();
+ for (int i = 0; i < interfaces.length; i++) {
+ if (list.contains(interfaces[i]) == false) {
+ list.add(interfaces[i]);
+ }
+ List superInterfaces = getAllInterfaces(interfaces[i]);
+ for (Iterator it = superInterfaces.iterator(); it.hasNext(); ) {
+ Class intface = (Class) it.next();
+ if (list.contains(intface) == false) {
+ list.add(intface);
+ }
+ }
+ }
+ cls = cls.getSuperclass();
+ }
+ return list;
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/DefaultOSGIKillbill.java b/osgi/src/main/java/org/killbill/billing/osgi/DefaultOSGIKillbill.java
new file mode 100644
index 0000000..b5dd39e
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/DefaultOSGIKillbill.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.catalog.api.CatalogUserApi;
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.entitlement.api.EntitlementApi;
+import org.killbill.billing.entitlement.api.SubscriptionApi;
+import org.killbill.billing.invoice.api.InvoicePaymentApi;
+import org.killbill.billing.invoice.api.InvoiceUserApi;
+import org.killbill.billing.osgi.api.OSGIKillbill;
+import org.killbill.billing.osgi.api.config.PluginConfigServiceApi;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.tenant.api.TenantUserApi;
+import org.killbill.billing.usage.api.UsageUserApi;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.ExportUserApi;
+import org.killbill.billing.util.api.RecordIdApi;
+import org.killbill.billing.util.api.TagUserApi;
+
+public class DefaultOSGIKillbill implements OSGIKillbill {
+
+ private final AccountUserApi accountUserApi;
+ private final CatalogUserApi catalogUserApi;
+ private final InvoicePaymentApi invoicePaymentApi;
+ private final InvoiceUserApi invoiceUserApi;
+ private final PaymentApi paymentApi;
+ private final TenantUserApi tenantUserApi;
+ private final UsageUserApi usageUserApi;
+ private final AuditUserApi auditUserApi;
+ private final CustomFieldUserApi customFieldUserApi;
+ private final ExportUserApi exportUserApi;
+ private final TagUserApi tagUserApi;
+ private final EntitlementApi entitlementApi;
+ private final SubscriptionApi subscriptionApi;
+ private final CurrencyConversionApi currencyConversionApi;
+ private final RecordIdApi recordIdApi;
+
+ private final PluginConfigServiceApi configServiceApi;
+
+ @Inject
+ public DefaultOSGIKillbill(final AccountUserApi accountUserApi,
+ final CatalogUserApi catalogUserApi,
+ final InvoicePaymentApi invoicePaymentApi,
+ final InvoiceUserApi invoiceUserApi,
+ final PaymentApi paymentApi,
+ final TenantUserApi tenantUserApi,
+ final UsageUserApi usageUserApi,
+ final AuditUserApi auditUserApi,
+ final CustomFieldUserApi customFieldUserApi,
+ final ExportUserApi exportUserApi,
+ final TagUserApi tagUserApi,
+ final EntitlementApi entitlementApi,
+ final SubscriptionApi subscriptionApi,
+ final RecordIdApi recordIdApi,
+ final CurrencyConversionApi currencyConversionApi,
+ final PluginConfigServiceApi configServiceApi) {
+ this.accountUserApi = accountUserApi;
+ this.catalogUserApi = catalogUserApi;
+ this.invoicePaymentApi = invoicePaymentApi;
+ this.invoiceUserApi = invoiceUserApi;
+ this.paymentApi = paymentApi;
+ this.tenantUserApi = tenantUserApi;
+ this.usageUserApi = usageUserApi;
+ this.auditUserApi = auditUserApi;
+ this.customFieldUserApi = customFieldUserApi;
+ this.exportUserApi = exportUserApi;
+ this.tagUserApi = tagUserApi;
+ this.entitlementApi = entitlementApi;
+ this.subscriptionApi = subscriptionApi;
+ this.currencyConversionApi = currencyConversionApi;
+ this.recordIdApi = recordIdApi;
+ this.configServiceApi = configServiceApi;
+ }
+
+ @Override
+ public AccountUserApi getAccountUserApi() {
+ return accountUserApi;
+ }
+
+ @Override
+ public CatalogUserApi getCatalogUserApi() {
+ return catalogUserApi;
+ }
+
+ @Override
+ public SubscriptionApi getSubscriptionApi() {
+ return subscriptionApi;
+ }
+
+ @Override
+ public InvoicePaymentApi getInvoicePaymentApi() {
+ return invoicePaymentApi;
+ }
+
+ @Override
+ public InvoiceUserApi getInvoiceUserApi() {
+ return invoiceUserApi;
+ }
+
+ @Override
+ public PaymentApi getPaymentApi() {
+ return paymentApi;
+ }
+
+ @Override
+ public TenantUserApi getTenantUserApi() {
+ return tenantUserApi;
+ }
+
+ @Override
+ public UsageUserApi getUsageUserApi() {
+ return usageUserApi;
+ }
+
+ @Override
+ public AuditUserApi getAuditUserApi() {
+ return auditUserApi;
+ }
+
+ @Override
+ public CustomFieldUserApi getCustomFieldUserApi() {
+ return customFieldUserApi;
+ }
+
+ @Override
+ public ExportUserApi getExportUserApi() {
+ return exportUserApi;
+ }
+
+ @Override
+ public TagUserApi getTagUserApi() {
+ return tagUserApi;
+ }
+
+ @Override
+ public EntitlementApi getEntitlementApi() {
+ return entitlementApi;
+ }
+
+ @Override
+ public RecordIdApi getRecordIdApi() {
+ return recordIdApi;
+ }
+
+ @Override
+ public CurrencyConversionApi getCurrencyConversionApi() {
+ return currencyConversionApi;
+ }
+
+ @Override
+ public PluginConfigServiceApi getPluginConfigServiceApi() {
+ return configServiceApi;
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/DefaultOSGIService.java b/osgi/src/main/java/org/killbill/billing/osgi/DefaultOSGIService.java
new file mode 100644
index 0000000..c649d4a
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/DefaultOSGIService.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.felix.framework.Felix;
+import org.apache.felix.framework.util.FelixConstants;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.launch.Framework;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.lifecycle.LifecycleHandlerType;
+import org.killbill.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+import org.killbill.billing.osgi.api.OSGIService;
+import org.killbill.billing.osgi.api.config.PluginConfigServiceApi;
+import org.killbill.billing.osgi.pluginconf.PluginFinder;
+import org.killbill.billing.util.config.OSGIConfig;
+
+import com.google.common.collect.ImmutableList;
+
+public class DefaultOSGIService implements OSGIService {
+
+ public static final String OSGI_SERVICE_NAME = "osgi-service";
+
+ private static final Logger logger = LoggerFactory.getLogger(DefaultOSGIService.class);
+
+ private final OSGIConfig osgiConfig;
+ private final KillbillActivator killbillActivator;
+ private final FileInstall fileInstall;
+ private final List<Bundle> installedBundles;
+
+ private Framework framework;
+
+ @Inject
+ public DefaultOSGIService(final OSGIConfig osgiConfig, final PureOSGIBundleFinder osgiBundleFinder,
+ final PluginFinder pluginFinder, final PluginConfigServiceApi pluginConfigServiceApi,
+ final KillbillActivator killbillActivator) {
+ this.osgiConfig = osgiConfig;
+ this.killbillActivator = killbillActivator;
+ this.fileInstall = new FileInstall(osgiBundleFinder, pluginFinder, pluginConfigServiceApi);
+ this.installedBundles = new LinkedList<Bundle>();
+ this.framework = null;
+ }
+
+ @Override
+ public String getName() {
+ return OSGI_SERVICE_NAME;
+ }
+
+
+ @LifecycleHandlerType(LifecycleLevel.INIT_PLUGIN)
+ public void initialize() {
+ try {
+ // We start by deleting existing osi cache; we might optimize later keeping the cache
+ pruneOSGICache();
+
+ // Create the system bundle for killbill and start the framework
+ this.framework = createAndInitFramework();
+ framework.start();
+
+ installedBundles.addAll(fileInstall.installBundles(framework));
+ } catch (BundleException e) {
+ logger.error("Failed to initialize Killbill OSGIService", e);
+ }
+
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.START_PLUGIN)
+ public void start() {
+ // This will call the start() method for the bundles
+ fileInstall.startBundles(installedBundles);
+ }
+
+
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_PLUGIN)
+ public void stop() {
+ try {
+ framework.stop();
+ framework.waitForStop(0);
+
+ installedBundles.clear();
+ } catch (BundleException e) {
+ logger.error("Failed to Stop Killbill OSGIService " + e.getMessage());
+ } catch (InterruptedException e) {
+ logger.error("Failed to Stop Killbill OSGIService " + e.getMessage());
+ }
+ }
+
+ private Framework createAndInitFramework() throws BundleException {
+ final Map<String, String> config = new HashMap<String, String>();
+ config.put("org.osgi.framework.system.packages.extra", osgiConfig.getSystemBundleExportPackages());
+ config.put("felix.cache.rootdir", osgiConfig.getOSGIBundleRootDir());
+ config.put("org.osgi.framework.storage", osgiConfig.getOSGIBundleCacheName());
+ return createAndInitFelixFrameworkWithSystemBundle(config);
+ }
+
+ private Framework createAndInitFelixFrameworkWithSystemBundle(final Map<String, String> config) throws BundleException {
+ // From standard properties add Felix specific property to add a System bundle activator
+ final Map<Object, Object> felixConfig = new HashMap<Object, Object>();
+ felixConfig.putAll(config);
+
+ // Install default bundles in the Framework: Killbill bundle only for now
+ // Note! Think twice before adding a bundle here as it will run inside the System bundle. This means the bundle
+ // callcontext that the bundle will see is the System bundle one, which will break e.g. resources lookup
+ felixConfig.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP,
+ ImmutableList.<BundleActivator>of(killbillActivator));
+
+ final Framework felix = new Felix(felixConfig);
+ felix.init();
+ return felix;
+ }
+
+ private void pruneOSGICache() {
+ final String path = osgiConfig.getOSGIBundleRootDir();
+ deleteUnderDirectory(new File(path));
+ }
+
+ private static void deleteUnderDirectory(final File path) {
+ deleteDirectory(path, false);
+ }
+
+ private static void deleteDirectory(final File path, final boolean deleteParent) {
+ if (path == null) {
+ return;
+ }
+
+ if (path.exists()) {
+ final File[] files = path.listFiles();
+ if (files != null) {
+ for (final File f : files) {
+ if (f.isDirectory()) {
+ deleteDirectory(f, true);
+ } else if (!f.delete()) {
+ logger.warn("Unable to delete {}", f.getAbsolutePath());
+ }
+ }
+ }
+
+ if (deleteParent) {
+ if (!path.delete()) {
+ logger.warn("Unable to delete {}", path.getAbsolutePath());
+ } else {
+ logger.info("Deleted recursively {}", path.getAbsolutePath());
+ }
+ }
+ }
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/DefaultOSGIServiceDescriptor.java b/osgi/src/main/java/org/killbill/billing/osgi/DefaultOSGIServiceDescriptor.java
new file mode 100644
index 0000000..ac5896f
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/DefaultOSGIServiceDescriptor.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi;
+
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+
+public class DefaultOSGIServiceDescriptor implements OSGIServiceDescriptor {
+
+ private final String pluginSymbolicName;
+ private final String serviceName;
+
+ public DefaultOSGIServiceDescriptor(final String pluginSymbolicName, final String serviceName) {
+ this.pluginSymbolicName = pluginSymbolicName;
+ this.serviceName = serviceName;
+ }
+
+ @Override
+ public String getPluginSymbolicName() {
+ return pluginSymbolicName;
+ }
+
+ @Override
+ public String getRegistrationName() {
+ return serviceName;
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/FileInstall.java b/osgi/src/main/java/org/killbill/billing/osgi/FileInstall.java
new file mode 100644
index 0000000..0b889b5
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/FileInstall.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import javax.annotation.Nullable;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.wiring.BundleRevision;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.osgi.api.config.PluginConfigServiceApi;
+import org.killbill.billing.osgi.api.config.PluginJavaConfig;
+import org.killbill.billing.osgi.api.config.PluginRubyConfig;
+import org.killbill.billing.osgi.pluginconf.DefaultPluginConfigServiceApi;
+import org.killbill.billing.osgi.pluginconf.PluginConfigException;
+import org.killbill.billing.osgi.pluginconf.PluginFinder;
+
+import com.google.common.io.ByteStreams;
+
+// TODO Pierre Should we leverage org.apache.felix.fileinstall.internal.FileInstall?
+public class FileInstall {
+
+ private static final Logger logger = LoggerFactory.getLogger(FileInstall.class);
+
+ private final PureOSGIBundleFinder osgiBundleFinder;
+ private final PluginFinder pluginFinder;
+ private final PluginConfigServiceApi pluginConfigServiceApi;
+
+ public FileInstall(final PureOSGIBundleFinder osgiBundleFinder, final PluginFinder pluginFinder, final PluginConfigServiceApi pluginConfigServiceApi) {
+ this.osgiBundleFinder = osgiBundleFinder;
+ this.pluginFinder = pluginFinder;
+ this.pluginConfigServiceApi = pluginConfigServiceApi;
+ }
+
+
+ public List<Bundle> installBundles(final Framework framework) {
+
+ final List<Bundle> installedBundles = new LinkedList<Bundle>();
+ try {
+
+ final BundleContext context = framework.getBundleContext();
+ final String jrubyBundlePath = findJrubyBundlePath();
+
+ // Install all bundles and create service mapping
+ installAllJavaBundles(context, installedBundles, jrubyBundlePath);
+ installAllJavaPluginBundles(context, installedBundles);
+ installAllJRubyPluginBundles(context, installedBundles, jrubyBundlePath);
+
+ } catch (PluginConfigException e) {
+ logger.error("Error while parsing plugin configurations", e);
+ } catch (BundleException e) {
+ logger.error("Error while parsing plugin configurations", e);
+ }
+ return installedBundles;
+ }
+
+ public void startBundles(final List<Bundle> installedBundles) {
+ // Start all the bundles
+ for (final Bundle bundle : installedBundles) {
+ startBundle(bundle);
+ }
+ }
+
+ private void installAllJavaBundles(final BundleContext context, final List<Bundle> installedBundles, @Nullable final String jrubyBundlePath) throws PluginConfigException, BundleException {
+ final List<String> bundleJarPaths = osgiBundleFinder.getLatestBundles();
+ for (final String cur : bundleJarPaths) {
+ // Don't install the jruby.jar bundle
+ if (jrubyBundlePath != null && jrubyBundlePath.equals(cur)) {
+ continue;
+ }
+
+ logger.info("Installing Java OSGI bundle from {}", cur);
+ final Bundle bundle = context.installBundle("file:" + cur);
+ installedBundles.add(bundle);
+ }
+ }
+
+ private void installAllJavaPluginBundles(final BundleContext context, final List<Bundle> installedBundles) throws PluginConfigException, BundleException {
+ final List<PluginJavaConfig> pluginJavaConfigs = pluginFinder.getLatestJavaPlugins();
+ for (final PluginJavaConfig cur : pluginJavaConfigs) {
+ logger.info("Installing Java bundle for plugin {} from {}", cur.getPluginName(), cur.getBundleJarPath());
+ final Bundle bundle = context.installBundle("file:" + cur.getBundleJarPath());
+ ((DefaultPluginConfigServiceApi) pluginConfigServiceApi).registerBundle(bundle.getBundleId(), cur);
+ installedBundles.add(bundle);
+ }
+ }
+
+ private void installAllJRubyPluginBundles(final BundleContext context, final List<Bundle> installedBundles, @Nullable final String jrubyBundlePath) throws PluginConfigException, BundleException {
+ if (jrubyBundlePath == null) {
+ return;
+ }
+
+ final List<PluginRubyConfig> pluginRubyConfigs = pluginFinder.getLatestRubyPlugins();
+ int i = 0;
+ for (final PluginRubyConfig cur : pluginRubyConfigs) {
+
+ final String uniqueJrubyBundlePath = "jruby-" + cur.getPluginName();
+
+ InputStream tweakedInputStream = null;
+ try {
+ logger.info("Installing JRuby bundle for plugin {} ", uniqueJrubyBundlePath);
+ tweakedInputStream = tweakRubyManifestToBeUnique(jrubyBundlePath, ++i);
+ final Bundle bundle = context.installBundle(uniqueJrubyBundlePath, tweakedInputStream);
+ ((DefaultPluginConfigServiceApi) pluginConfigServiceApi).registerBundle(bundle.getBundleId(), cur);
+ installedBundles.add(bundle);
+ } catch (IOException e) {
+ logger.warn("Failed to open file {}", jrubyBundlePath);
+ } finally {
+ if (tweakedInputStream != null) {
+ try {
+ tweakedInputStream.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+ }
+ }
+
+
+ private InputStream tweakRubyManifestToBeUnique(final String rubyJar, int index) throws IOException {
+
+ final Attributes.Name attrName = new Attributes.Name(Constants.BUNDLE_SYMBOLICNAME);
+ final JarInputStream in = new JarInputStream(new FileInputStream(new File(rubyJar)));
+ final Manifest manifest = in.getManifest();
+
+
+ final Object currentValue = manifest.getMainAttributes().get(attrName);
+ manifest.getMainAttributes().put(attrName, currentValue.toString() + "-" + index);
+
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ final JarOutputStream jarOut = new JarOutputStream(out, manifest);
+ try {
+ JarEntry e = in.getNextJarEntry();
+ while (e != null) {
+ if (!e.getName().equals(JarFile.MANIFEST_NAME)) {
+ jarOut.putNextEntry(e);
+ ByteStreams.copy(in, jarOut);
+ }
+ e = in.getNextJarEntry();
+ }
+
+ } finally {
+ if (jarOut != null) {
+ jarOut.close();
+ }
+ }
+
+ return new ByteArrayInputStream(out.toByteArray());
+ }
+
+ private String findJrubyBundlePath() {
+ final String expectedPath = osgiBundleFinder.getPlatformOSGIBundlesRootDir() + "jruby.jar";
+ if (new File(expectedPath).isFile()) {
+ return expectedPath;
+ } else {
+ logger.warn("Unable to find the JRuby bundle at {}, ruby plugins won't be started!", expectedPath);
+ return null;
+ }
+ }
+
+ private boolean startBundle(final Bundle bundle) {
+ if (bundle.getState() == Bundle.UNINSTALLED) {
+ logger.info("Skipping uninstalled bundle {}", bundle.getLocation());
+ } else if (isFragment(bundle)) {
+ // Fragments can never be started.
+ logger.info("Skipping fragment bundle {}", bundle.getLocation());
+ } else {
+ logger.info("Starting bundle {}", bundle.getLocation());
+ try {
+ bundle.start();
+ return true;
+ } catch (BundleException e) {
+ logger.warn("Unable to start bundle", e);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if a bundle is a fragment.
+ *
+ * @param bundle bundle to check
+ * @return true iff the bundle is a fragment
+ */
+ private boolean isFragment(final Bundle bundle) {
+ // Necessary cast on jdk7
+ final BundleRevision bundleRevision = (BundleRevision) bundle.adapt(BundleRevision.class);
+ return bundleRevision != null && (bundleRevision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0;
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/glue/DefaultOSGIModule.java b/osgi/src/main/java/org/killbill/billing/osgi/glue/DefaultOSGIModule.java
new file mode 100644
index 0000000..0d9267f
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/glue/DefaultOSGIModule.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.glue;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServlet;
+import javax.sql.DataSource;
+
+import org.osgi.service.http.HttpService;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.osgi.DefaultOSGIKillbill;
+import org.killbill.billing.osgi.DefaultOSGIService;
+import org.killbill.billing.osgi.KillbillActivator;
+import org.killbill.billing.osgi.KillbillEventObservable;
+import org.killbill.billing.osgi.PureOSGIBundleFinder;
+import org.killbill.billing.osgi.api.DefaultOSGIUserApi;
+import org.killbill.billing.osgi.api.OSGIKillbill;
+import org.killbill.billing.osgi.api.OSGIService;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.osgi.api.OSGIUserApi;
+import org.killbill.billing.osgi.api.config.PluginConfigServiceApi;
+import org.killbill.billing.osgi.http.DefaultHttpService;
+import org.killbill.billing.osgi.http.DefaultServletRouter;
+import org.killbill.billing.osgi.http.OSGIServlet;
+import org.killbill.billing.osgi.pluginconf.DefaultPluginConfigServiceApi;
+import org.killbill.billing.osgi.pluginconf.PluginFinder;
+import org.killbill.billing.util.config.OSGIConfig;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+
+public class DefaultOSGIModule extends AbstractModule {
+
+ public static final String OSGI_NAMED = "osgi";
+
+ protected final ConfigSource configSource;
+
+ public DefaultOSGIModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ protected void installConfig() {
+ final OSGIConfig config = new ConfigurationObjectFactory(configSource).build(OSGIConfig.class);
+ bind(OSGIConfig.class).toInstance(config);
+
+ final OSGIDataSourceConfig osgiDataSourceConfig = new ConfigurationObjectFactory(configSource).build(OSGIDataSourceConfig.class);
+ bind(OSGIDataSourceConfig.class).toInstance(osgiDataSourceConfig);
+ }
+
+ protected void installOSGIServlet() {
+ bind(new TypeLiteral<OSGIServiceRegistration<Servlet>>() {}).to(DefaultServletRouter.class).asEagerSingleton();
+ bind(HttpServlet.class).annotatedWith(Names.named(OSGI_NAMED)).to(OSGIServlet.class).asEagerSingleton();
+ }
+
+ protected void installHttpService() {
+ bind(HttpService.class).to(DefaultHttpService.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ installConfig();
+ installOSGIServlet();
+ installHttpService();
+
+ bind(OSGIService.class).to(DefaultOSGIService.class).asEagerSingleton();
+
+ bind(OSGIUserApi.class).to(DefaultOSGIUserApi.class).asEagerSingleton();
+ bind(KillbillActivator.class).asEagerSingleton();
+ bind(PureOSGIBundleFinder.class).asEagerSingleton();
+ bind(PluginFinder.class).asEagerSingleton();
+ bind(PluginConfigServiceApi.class).to(DefaultPluginConfigServiceApi.class).asEagerSingleton();
+ bind(OSGIKillbill.class).to(DefaultOSGIKillbill.class).asEagerSingleton();
+ bind(OSGIDataSourceProvider.class).asEagerSingleton();
+ bind(KillbillEventObservable.class).asEagerSingleton();
+ bind(DataSource.class).annotatedWith(Names.named(OSGI_NAMED)).toProvider(OSGIDataSourceProvider.class).asEagerSingleton();
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/glue/OSGIDataSourceConfig.java b/osgi/src/main/java/org/killbill/billing/osgi/glue/OSGIDataSourceConfig.java
new file mode 100644
index 0000000..1bbfc21
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/glue/OSGIDataSourceConfig.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.glue;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+import org.skife.config.TimeSpan;
+
+public interface OSGIDataSourceConfig {
+
+ static String DATA_SOURCE_PROP_PREFIX = "org.killbill.billing.osgi.";
+
+ @Description("The jdbc url for the database")
+ @Config(DATA_SOURCE_PROP_PREFIX + "jdbc.url")
+ @Default("jdbc:mysql://127.0.0.1:3306/killbill")
+ String getJdbcUrl();
+
+ @Description("The jdbc user name for the database")
+ @Config(DATA_SOURCE_PROP_PREFIX + "jdbc.user")
+ @Default("root")
+ String getUsername();
+
+ @Description("The jdbc password for the database")
+ @Config(DATA_SOURCE_PROP_PREFIX + "jdbc.password")
+ @Default("root")
+ String getPassword();
+
+ @Description("The minimum allowed number of idle connections to the database")
+ @Config(DATA_SOURCE_PROP_PREFIX + "jdbc.minIdle")
+ @Default("1")
+ int getMinIdle();
+
+ @Description("The maximum allowed number of active connections to the database")
+ @Config(DATA_SOURCE_PROP_PREFIX + "jdbc.maxActive")
+ @Default("10")
+ int getMaxActive();
+
+ @Description("How long to wait before a connection attempt to the database is considered timed out")
+ @Config(DATA_SOURCE_PROP_PREFIX + "jdbc.connectionTimeout")
+ @Default("10s")
+ TimeSpan getConnectionTimeout();
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/glue/OSGIDataSourceProvider.java b/osgi/src/main/java/org/killbill/billing/osgi/glue/OSGIDataSourceProvider.java
new file mode 100644
index 0000000..7bbcff8
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/glue/OSGIDataSourceProvider.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.glue;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Provider;
+import javax.sql.DataSource;
+
+import org.skife.config.TimeSpan;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.tweak.SQLLog;
+
+import org.killbill.billing.util.dao.DateTimeArgumentFactory;
+import org.killbill.billing.util.dao.DateTimeZoneArgumentFactory;
+import org.killbill.billing.util.dao.EnumArgumentFactory;
+import org.killbill.billing.util.dao.LocalDateArgumentFactory;
+import org.killbill.billing.util.dao.UUIDArgumentFactory;
+import org.killbill.billing.util.dao.UuidMapper;
+
+import com.google.inject.Inject;
+import com.jolbox.bonecp.BoneCPConfig;
+import com.jolbox.bonecp.BoneCPDataSource;
+import com.mchange.v2.c3p0.ComboPooledDataSource;
+
+public class OSGIDataSourceProvider implements Provider<DataSource> {
+
+ private final OSGIDataSourceConfig config;
+ private SQLLog sqlLog;
+
+ @Inject
+ public OSGIDataSourceProvider(final OSGIDataSourceConfig config) {
+ this.config = config;
+ }
+
+ @Inject(optional = true)
+ public void setSqlLog(final SQLLog sqlLog) {
+ this.sqlLog = sqlLog;
+ }
+
+ @Override
+ public DataSource get() {
+ final DataSource ds = getDataSource();
+
+ final DBI dbi = new DBI(ds);
+ dbi.registerArgumentFactory(new UUIDArgumentFactory());
+ dbi.registerArgumentFactory(new DateTimeZoneArgumentFactory());
+ dbi.registerArgumentFactory(new DateTimeArgumentFactory());
+ dbi.registerArgumentFactory(new LocalDateArgumentFactory());
+ dbi.registerArgumentFactory(new EnumArgumentFactory());
+ dbi.registerMapper(new UuidMapper());
+
+ if (sqlLog != null) {
+ dbi.setSQLLog(sqlLog);
+ }
+ return ds;
+ }
+
+ private DataSource getDataSource() {
+ final DataSource ds;
+
+ // TODO PIERRE DaoConfig is in the skeleton
+ final String dataSource = System.getProperty("com.ning.jetty.jdbi.datasource", "c3p0");
+ if (dataSource.equals("c3p0")) {
+ ds = getC3P0DataSource();
+ } else if (dataSource.equals("bonecp")) {
+ ds = getBoneCPDatSource();
+ } else {
+ throw new IllegalArgumentException("DataSource " + dataSource + " unsupported");
+ }
+
+ return ds;
+ }
+
+ private DataSource getBoneCPDatSource() {
+ final BoneCPConfig dbConfig = new BoneCPConfig();
+ dbConfig.setJdbcUrl(config.getJdbcUrl());
+ dbConfig.setUsername(config.getUsername());
+ dbConfig.setPassword(config.getPassword());
+ dbConfig.setMinConnectionsPerPartition(config.getMinIdle());
+ dbConfig.setMaxConnectionsPerPartition(config.getMaxActive());
+ dbConfig.setConnectionTimeout(config.getConnectionTimeout().getPeriod(), config.getConnectionTimeout().getUnit());
+ /*
+ dbConfig.setIdleMaxAge(config.getIdleMaxAge().getPeriod(), config.getIdleMaxAge().getUnit());
+ dbConfig.setMaxConnectionAge(config.getMaxConnectionAge().getPeriod(), config.getMaxConnectionAge().getUnit());
+ dbConfig.setIdleConnectionTestPeriod(config.getIdleConnectionTestPeriod().getPeriod(), config.getIdleConnectionTestPeriod().getUnit());
+ */
+ dbConfig.setPartitionCount(1);
+ dbConfig.setDisableJMX(false);
+
+ return new BoneCPDataSource(dbConfig);
+ }
+
+ private DataSource getC3P0DataSource() {
+ final ComboPooledDataSource cpds = new ComboPooledDataSource();
+ cpds.setJdbcUrl(config.getJdbcUrl());
+ cpds.setUser(config.getUsername());
+ cpds.setPassword(config.getPassword());
+ // http://www.mchange.com/projects/c3p0/#minPoolSize
+ // Minimum number of Connections a pool will maintain at any given time.
+ cpds.setMinPoolSize(config.getMinIdle());
+ // http://www.mchange.com/projects/c3p0/#maxPoolSize
+ // Maximum number of Connections a pool will maintain at any given time.
+ cpds.setMaxPoolSize(config.getMaxActive());
+ // http://www.mchange.com/projects/c3p0/#checkoutTimeout
+ // The number of milliseconds a client calling getConnection() will wait for a Connection to be checked-in or
+ // acquired when the pool is exhausted. Zero means wait indefinitely. Setting any positive value will cause the getConnection()
+ // call to time-out and break with an SQLException after the specified number of milliseconds.
+ cpds.setCheckoutTimeout(toMilliSeconds(config.getConnectionTimeout()));
+ // http://www.mchange.com/projects/c3p0/#maxIdleTime
+ // Seconds a Connection can remain pooled but unused before being discarded. Zero means idle connections never expire.
+ // cpds.setMaxIdleTime(toSeconds(config.getIdleMaxAge()));
+ // http://www.mchange.com/projects/c3p0/#maxConnectionAge
+ // Seconds, effectively a time to live. A Connection older than maxConnectionAge will be destroyed and purged from the pool.
+ // This differs from maxIdleTime in that it refers to absolute age. Even a Connection which has not been much idle will be purged
+ // from the pool if it exceeds maxConnectionAge. Zero means no maximum absolute age is enforced.
+ // cpds.setMaxConnectionAge(toSeconds(config.getMaxConnectionAge()));
+ // http://www.mchange.com/projects/c3p0/#idleConnectionTestPeriod
+ // If this is a number greater than 0, c3p0 will test all idle, pooled but unchecked-out connections, every this number of seconds.
+ cpds.setIdleConnectionTestPeriod(60);
+
+ return cpds;
+ }
+
+ private int toSeconds(final TimeSpan timeSpan) {
+ return toSeconds(timeSpan.getPeriod(), timeSpan.getUnit());
+ }
+
+ private int toSeconds(final long period, final TimeUnit timeUnit) {
+ return (int) TimeUnit.SECONDS.convert(period, timeUnit);
+ }
+
+ private int toMilliSeconds(final TimeSpan timeSpan) {
+ return toMilliSeconds(timeSpan.getPeriod(), timeSpan.getUnit());
+ }
+
+ private int toMilliSeconds(final long period, final TimeUnit timeUnit) {
+ return (int) TimeUnit.MILLISECONDS.convert(period, timeUnit);
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/http/DefaultHttpContext.java b/osgi/src/main/java/org/killbill/billing/osgi/http/DefaultHttpContext.java
new file mode 100644
index 0000000..8aba780
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/http/DefaultHttpContext.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.http;
+
+import java.io.IOException;
+import java.net.URL;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.osgi.service.http.HttpContext;
+
+public class DefaultHttpContext implements HttpContext {
+
+ @Override
+ public boolean handleSecurity(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
+ // Security should have already been handled by Shiro
+ return true;
+ }
+
+ @Override
+ public URL getResource(final String name) {
+ // Maybe it's in our classpath?
+ return DefaultHttpContext.class.getClassLoader().getResource(name);
+ }
+
+ @Override
+ public String getMimeType(final String name) {
+ return null;
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/http/DefaultHttpService.java b/osgi/src/main/java/org/killbill/billing/osgi/http/DefaultHttpService.java
new file mode 100644
index 0000000..c752094
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/http/DefaultHttpService.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.http;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+
+import org.killbill.billing.osgi.ContextClassLoaderHelper;
+
+@Singleton
+public class DefaultHttpService implements HttpService {
+
+ private final DefaultServletRouter servletRouter;
+
+ @Inject
+ public DefaultHttpService(final DefaultServletRouter servletRouter) {
+ this.servletRouter = servletRouter;
+ }
+
+ @Override
+ public void registerServlet(final String alias, final Servlet servlet, final Dictionary initparams, final HttpContext httpContext) throws ServletException, NamespaceException {
+
+ if (alias == null) {
+ throw new IllegalArgumentException("Invalid alias (null)");
+ } else if (servlet == null) {
+ throw new IllegalArgumentException("Invalid servlet (null)");
+ }
+ final Servlet wrappedServlet = ContextClassLoaderHelper.getWrappedServiceWithCorrectContextClassLoader(servlet);
+
+ servletRouter.registerServiceFromPath(alias, wrappedServlet);
+ }
+
+ @Override
+ public void registerResources(final String alias, final String name, final HttpContext httpContext) throws NamespaceException {
+ final Servlet staticServlet = new StaticServlet(httpContext);
+ try {
+ registerServlet(alias, staticServlet, new Hashtable(), httpContext);
+ } catch (ServletException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public void unregister(final String alias) {
+ servletRouter.unregisterServiceFromPath(alias);
+ }
+
+ @Override
+ public HttpContext createDefaultHttpContext() {
+ return new DefaultHttpContext();
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/http/DefaultServletRouter.java b/osgi/src/main/java/org/killbill/billing/osgi/http/DefaultServletRouter.java
new file mode 100644
index 0000000..310b81c
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/http/DefaultServletRouter.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.http;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Singleton;
+import javax.servlet.Servlet;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+
+@Singleton
+public class DefaultServletRouter implements OSGIServiceRegistration<Servlet> {
+
+ private static final Logger logger = LoggerFactory.getLogger(DefaultServletRouter.class);
+
+ // Internal Servlet routing table: map of plugin prefixes to servlet instances.
+ // A plugin prefix can be /foo, /foo/bar, /foo/bar/baz, ... and is mounted on /plugins/<pluginPrefix>
+ private final Map<String, Servlet> pluginPathServlets = new HashMap<String, Servlet>();
+ private final Map<String, OSGIServiceDescriptor> pluginRegistrations = new HashMap<String, OSGIServiceDescriptor>();
+
+ @Override
+ public void registerService(final OSGIServiceDescriptor desc, final Servlet httpServlet) {
+ // Enforce each route to start with /
+ final String pathPrefix = getPathPrefixFromDescriptor(desc);
+ if (pathPrefix == null) {
+ logger.warn("Skipping registration of OSGI servlet for service {} (service info is not specified)", desc.getRegistrationName());
+ return;
+ }
+
+ logger.info("Registering OSGI servlet at " + pathPrefix);
+ synchronized (this) {
+ registerServletInternal(pathPrefix, httpServlet);
+ registerServiceInternal(desc);
+ }
+ }
+
+ public void registerServiceFromPath(final String path, final Servlet httpServlet) {
+ final String pathPrefix = sanitizePathPrefix(path);
+ registerServletInternal(pathPrefix, httpServlet);
+ }
+
+ private void registerServletInternal(final String pathPrefix, final Servlet httpServlet) {
+ pluginPathServlets.put(pathPrefix, httpServlet);
+ }
+
+ private void registerServiceInternal(final OSGIServiceDescriptor desc) {
+ pluginRegistrations.put(desc.getRegistrationName(), desc);
+ }
+
+ @Override
+ public void unregisterService(final String serviceName) {
+ synchronized (this) {
+ final OSGIServiceDescriptor desc = pluginRegistrations.get(serviceName);
+ if (desc != null) {
+ final String pathPrefix = getPathPrefixFromDescriptor(desc);
+ if (pathPrefix == null) {
+ logger.warn("Skipping unregistration of OSGI servlet for service {} (service info is not specified)", desc.getRegistrationName());
+ return;
+ }
+
+ logger.info("Unregistering OSGI servlet " + desc.getRegistrationName() + " at path " + pathPrefix);
+ synchronized (this) {
+ unRegisterServletInternal(pathPrefix);
+ unRegisterServiceInternal(desc);
+ }
+ }
+ }
+ }
+
+ public void unregisterServiceFromPath(final String path) {
+ final String pathPrefix = sanitizePathPrefix(path);
+ unRegisterServletInternal(pathPrefix);
+ }
+
+ private Servlet unRegisterServletInternal(final String pathPrefix) {
+ return pluginPathServlets.remove(pathPrefix);
+ }
+
+ private OSGIServiceDescriptor unRegisterServiceInternal(final OSGIServiceDescriptor desc) {
+ return pluginRegistrations.remove(desc.getRegistrationName());
+ }
+
+ @Override
+ public Servlet getServiceForName(final String serviceName) {
+ final OSGIServiceDescriptor desc = pluginRegistrations.get(serviceName);
+ if (desc == null) {
+ return null;
+ }
+ final String registeredPath = getPathPrefixFromDescriptor(desc);
+ return pluginPathServlets.get(registeredPath);
+ }
+
+ private String getPathPrefixFromDescriptor(final OSGIServiceDescriptor desc) {
+ return sanitizePathPrefix(desc.getRegistrationName());
+ }
+
+ public Servlet getServiceForPath(final String path) {
+ return getServletForPathPrefix(path);
+ }
+
+ @Override
+ public Set<String> getAllServices() {
+ return pluginPathServlets.keySet();
+ }
+
+ @Override
+ public Class<Servlet> getServiceType() {
+ return Servlet.class;
+ }
+
+ // TODO PIERRE Naive implementation - we should rather switch to e.g. heap tree
+ public String getPluginPrefixForPath(final String pathPrefix) {
+ String bestMatch = null;
+ for (final String potentialMatch : pluginPathServlets.keySet()) {
+ if (pathPrefix.startsWith(potentialMatch) && (bestMatch == null || bestMatch.length() < potentialMatch.length())) {
+ bestMatch = potentialMatch;
+ }
+ }
+ return bestMatch;
+ }
+
+ private Servlet getServletForPathPrefix(final String pathPrefix) {
+ final String bestMatch = getPluginPrefixForPath(pathPrefix);
+ return bestMatch == null ? null : pluginPathServlets.get(bestMatch);
+ }
+
+ private static String sanitizePathPrefix(final String inputPath) {
+ if (inputPath == null) {
+ return null;
+ }
+
+ final String pathPrefix;
+ if (inputPath.charAt(0) != '/') {
+ pathPrefix = "/" + inputPath;
+ } else {
+ pathPrefix = inputPath;
+ }
+ return pathPrefix;
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/http/OSGIServlet.java b/osgi/src/main/java/org/killbill/billing/osgi/http/OSGIServlet.java
new file mode 100644
index 0000000..b5e2a83
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/http/OSGIServlet.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.http;
+
+import java.io.IOException;
+import java.util.Vector;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class OSGIServlet extends HttpServlet {
+
+ private final Vector<Servlet> initializedServlets = new Vector<Servlet>();
+ private final Object servletsMonitor = new Object();
+
+ @Inject
+ private DefaultServletRouter servletRouter;
+
+ @Override
+ protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ serviceViaPlugin(req, resp);
+ }
+
+ @Override
+ protected void doHead(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ serviceViaPlugin(req, resp);
+ }
+
+ @Override
+ protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ serviceViaPlugin(req, resp);
+ }
+
+ @Override
+ protected void doPut(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ serviceViaPlugin(req, resp);
+ }
+
+ @Override
+ protected void doDelete(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ serviceViaPlugin(req, resp);
+ }
+
+ @Override
+ protected void doOptions(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ serviceViaPlugin(req, resp);
+ }
+
+ private void serviceViaPlugin(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ // requestPath is the full path minus the JAX-RS prefix (/plugins)
+ final String requestPath = req.getServletPath() + req.getPathInfo();
+
+
+ final Servlet pluginServlet = getPluginServlet(requestPath);
+
+
+ if (pluginServlet != null) {
+ initializeServletIfNeeded(req, pluginServlet);
+ final OSGIServletRequestWrapper requestWrapper = new OSGIServletRequestWrapper(req, servletRouter.getPluginPrefixForPath(requestPath));
+ pluginServlet.service(requestWrapper, resp);
+ } else {
+ resp.sendError(404);
+ }
+ }
+
+ // Request wrapper to hide the plugin prefix to OSGI servlets (the plugin prefix serves as a servlet path)
+ private static final class OSGIServletRequestWrapper extends HttpServletRequestWrapper {
+
+ private final String pluginPrefix;
+
+ public OSGIServletRequestWrapper(final HttpServletRequest request, final String pluginPrefix) {
+ super(request);
+ this.pluginPrefix = pluginPrefix;
+ }
+
+ @Override
+ public String getPathInfo() {
+ return super.getPathInfo().replace(pluginPrefix, "");
+ }
+
+ @Override
+ public String getContextPath() {
+ return super.getContextPath() + pluginPrefix;
+ }
+ }
+
+ // Hack to bridge the gap between the web container and the OSGI servlets
+ private void initializeServletIfNeeded(final HttpServletRequest req, final Servlet pluginServlet) throws ServletException {
+ if (!initializedServlets.contains(pluginServlet)) {
+ synchronized (servletsMonitor) {
+ if (!initializedServlets.contains(pluginServlet)) {
+ final ServletConfig servletConfig = (ServletConfig) req.getAttribute("killbill.osgi.servletConfig");
+ if (servletConfig != null) {
+ // TODO PIERRE The servlet will never be destroyed!
+ pluginServlet.init(servletConfig);
+ initializedServlets.add(pluginServlet);
+ }
+ }
+ }
+ }
+ }
+
+ private Servlet getPluginServlet(final String requestPath) {
+ if (requestPath != null) {
+ return servletRouter.getServiceForPath(requestPath);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/http/StaticServlet.java b/osgi/src/main/java/org/killbill/billing/osgi/http/StaticServlet.java
new file mode 100644
index 0000000..6baa92f
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/http/StaticServlet.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.http;
+
+import java.io.IOException;
+import java.net.URL;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+import org.osgi.service.http.HttpContext;
+
+import com.google.common.io.Resources;
+
+// Simple servlet to serve OSGI resources
+public class StaticServlet extends HttpServlet {
+
+ private final HttpContext httpContext;
+
+ public StaticServlet(final HttpContext httpContext) {
+ this.httpContext = httpContext;
+ }
+
+ @Override
+ protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ final URL url = findResourceURL(req);
+ if (url != null) {
+ Resources.copy(url, resp.getOutputStream());
+ resp.setStatus(200);
+ return;
+ }
+
+ // If we can't find it, the container might
+ final RequestDispatcher rd = getServletContext().getNamedDispatcher("default");
+ final HttpServletRequest wrapped = new HttpServletRequestWrapper(req) {
+ public String getServletPath() { return ""; }
+ };
+ rd.forward(wrapped, resp);
+ }
+
+ // TODO PIERRE HUGE HACK
+ // We don't really know at this point the resource path to look for
+ // e.g. if the request is for /plugins/foo/bar/baz/qux.css, should
+ // we look for /qux.css? /baz/qux.css? /bar/baz/qux.css? /foo/bar/baz/qux.css?
+ private URL findResourceURL(final HttpServletRequest request) {
+ final String url = request.getRequestURI();
+ for (int i = 0; i < url.lastIndexOf('/'); i++) {
+ final int idx = url.indexOf('/', i);
+ if (idx > -1) {
+ final String resourceName = url.substring(idx);
+ final URL match = findResourceURL(resourceName);
+ if (match != null) {
+ return match;
+ }
+ }
+ }
+ return null;
+ }
+
+ private URL findResourceURL(final String resourceName) {
+ URL url = httpContext.getResource(resourceName);
+ if (url == null) {
+ // Look into the OSGI bundle JAR
+ url = httpContext.getClass().getResource(resourceName);
+ }
+ return url;
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/KillbillActivator.java b/osgi/src/main/java/org/killbill/billing/osgi/KillbillActivator.java
new file mode 100644
index 0000000..9a6da80
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/KillbillActivator.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Observable;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.servlet.Servlet;
+import javax.sql.DataSource;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.http.HttpService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.currency.plugin.api.CurrencyPluginApi;
+import org.killbill.billing.osgi.api.OSGIKillbill;
+import org.killbill.billing.osgi.api.OSGIPluginProperties;
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.osgi.glue.DefaultOSGIModule;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillRegistrar;
+
+import com.google.common.collect.ImmutableList;
+
+public class KillbillActivator implements BundleActivator, ServiceListener {
+
+ final static int PLUGIN_NAME_MAX_LENGTH = 40;
+ final static Pattern PLUGIN_NAME_PATTERN = Pattern.compile("\\p{Lower}(?:\\p{Lower}|\\d|-|_)*");
+
+ private final static Logger logger = LoggerFactory.getLogger(KillbillActivator.class);
+
+ private final OSGIKillbill osgiKillbill;
+ private final HttpService defaultHttpService;
+ private final DataSource dataSource;
+ private final KillbillEventObservable observable;
+ private final OSGIKillbillRegistrar registrar;
+
+ private final List<OSGIServiceRegistration> allRegistrationHandlers;
+
+
+ private BundleContext context = null;
+
+ @Inject
+ public KillbillActivator(@Named(DefaultOSGIModule.OSGI_NAMED) final DataSource dataSource,
+ final OSGIKillbill osgiKillbill,
+ final HttpService defaultHttpService,
+ final KillbillEventObservable observable,
+ final OSGIServiceRegistration<Servlet> servletRouter,
+ final OSGIServiceRegistration<PaymentPluginApi> paymentProviderPluginRegistry,
+ final OSGIServiceRegistration<CurrencyPluginApi> currencyProviderPluginRegistry) {
+ this.osgiKillbill = osgiKillbill;
+ this.defaultHttpService = defaultHttpService;
+ this.dataSource = dataSource;
+ this.observable = observable;
+ this.registrar = new OSGIKillbillRegistrar();
+ this.allRegistrationHandlers = ImmutableList.<OSGIServiceRegistration>of(servletRouter, paymentProviderPluginRegistry, currencyProviderPluginRegistry);
+ }
+
+ @Override
+ public void start(final BundleContext context) throws Exception {
+
+ this.context = context;
+ final Dictionary props = new Hashtable();
+ props.put(OSGIPluginProperties.PLUGIN_NAME_PROP, "killbill");
+
+ observable.register();
+
+ registrar.registerService(context, OSGIKillbill.class, osgiKillbill, props);
+ registrar.registerService(context, HttpService.class, defaultHttpService, props);
+ registrar.registerService(context, Observable.class, observable, props);
+ registrar.registerService(context, DataSource.class, dataSource, props);
+
+ context.addServiceListener(this);
+ }
+
+ @Override
+ public void stop(final BundleContext context) throws Exception {
+ this.context = null;
+ context.removeServiceListener(this);
+ observable.unregister();
+ registrar.unregisterAll();
+ }
+
+ @Override
+ public void serviceChanged(final ServiceEvent event) {
+ if (context == null || (event.getType() != ServiceEvent.REGISTERED && event.getType() != ServiceEvent.UNREGISTERING)) {
+ // We are not initialized or uninterested
+ return;
+ }
+
+ final ServiceReference serviceReference = event.getServiceReference();
+ for (OSGIServiceRegistration cur : allRegistrationHandlers) {
+ if (listenForServiceType(serviceReference, event.getType(), cur.getServiceType(), cur)) {
+ break;
+ }
+ }
+ }
+
+ private <T> boolean listenForServiceType(final ServiceReference serviceReference, final int eventType, final Class<T> claz, final OSGIServiceRegistration<T> registration) {
+ // Make sure we can retrieve the plugin name
+ final String serviceName = (String) serviceReference.getProperty(OSGIPluginProperties.PLUGIN_NAME_PROP);
+ if (serviceName == null || !checkSanityPluginRegistrationName(serviceName)) {
+ // Quite common for non Killbill bundles
+ logger.debug("Ignoring registered OSGI service {} with no {} property", claz.getName(), OSGIPluginProperties.PLUGIN_NAME_PROP);
+ return true;
+ }
+
+ final Object theServiceObject = context.getService(serviceReference);
+ // Is that for us? We look for a subclass here for greater flexibility (e.g. HttpServlet for a Servlet service)
+ if (theServiceObject == null || !claz.isAssignableFrom(theServiceObject.getClass())) {
+ return false;
+ }
+ final T theService = (T) theServiceObject;
+
+ final OSGIServiceDescriptor desc = new DefaultOSGIServiceDescriptor(serviceReference.getBundle().getSymbolicName(), serviceName);
+ switch (eventType) {
+ case ServiceEvent.REGISTERED:
+ final T wrappedService = ContextClassLoaderHelper.getWrappedServiceWithCorrectContextClassLoader(theService);
+ registration.registerService(desc, wrappedService);
+ break;
+ case ServiceEvent.UNREGISTERING:
+ registration.unregisterService(desc.getRegistrationName());
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+
+ private final boolean checkSanityPluginRegistrationName(final String pluginName) {
+ final Matcher m = PLUGIN_NAME_PATTERN.matcher(pluginName);
+ if (!m.matches()) {
+ logger.warn("Invalid plugin name {} : should be of the form {}", pluginName, PLUGIN_NAME_PATTERN.toString());
+ return false;
+ }
+ if (pluginName.length() > PLUGIN_NAME_MAX_LENGTH) {
+ logger.warn("Invalid plugin name {} : too long, should be less than {}", pluginName, PLUGIN_NAME_MAX_LENGTH);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/KillbillEventObservable.java b/osgi/src/main/java/org/killbill/billing/osgi/KillbillEventObservable.java
new file mode 100644
index 0000000..777527f
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/KillbillEventObservable.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi;
+
+import java.util.Observable;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.bus.api.PersistentBus.EventBusException;
+import org.killbill.billing.notification.plugin.api.ExtBusEvent;
+
+import com.google.common.eventbus.Subscribe;
+
+public class KillbillEventObservable extends Observable {
+
+
+ private Logger logger = LoggerFactory.getLogger(KillbillEventObservable.class);
+
+ private final PersistentBus externalBus;
+
+ @Inject
+ public KillbillEventObservable(@Named("externalBus") final PersistentBus externalBus) {
+ this.externalBus = externalBus;
+ }
+
+ public void register() throws EventBusException {
+ externalBus.register(this);
+ }
+
+ public void unregister() throws EventBusException {
+ deleteObservers();
+ if (externalBus != null) {
+ externalBus.unregister(this);
+ }
+ }
+
+ @Subscribe
+ public void handleKillbillEvent(final ExtBusEvent event) {
+
+ logger.debug("Received external event " + event.toString());
+ setChanged();
+ notifyObservers(event);
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginConfig.java b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginConfig.java
new file mode 100644
index 0000000..2645289
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginConfig.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.pluginconf;
+
+import java.io.File;
+import java.util.Properties;
+
+import org.killbill.billing.osgi.api.config.PluginConfig;
+
+public abstract class DefaultPluginConfig implements PluginConfig {
+
+ private static final String PROP_PLUGIN_TYPE_NAME = "pluginType";
+
+ private final String pluginName;
+ private final PluginType pluginType;
+ private final String version;
+ private final File pluginVersionRoot;
+
+ public DefaultPluginConfig(final String pluginName, final String version, final Properties props, final File pluginVersionRoot) {
+ this.pluginName = pluginName;
+ this.version = version;
+ this.pluginVersionRoot = pluginVersionRoot;
+ this.pluginType = PluginType.valueOf(props.getProperty(PROP_PLUGIN_TYPE_NAME, PluginType.__UNKNOWN__.toString()));
+ }
+
+ @Override
+ public String getPluginName() {
+ return pluginName;
+ }
+
+ @Override
+ public PluginType getPluginType() {
+ return pluginType;
+ }
+
+ @Override
+ public String getVersion() {
+ return version;
+ }
+
+ @Override
+ public String getPluginVersionnedName() {
+ return pluginName + "-" + version;
+ }
+
+ @Override
+ public File getPluginVersionRoot() {
+ return pluginVersionRoot;
+ }
+
+ @Override
+ public abstract PluginLanguage getPluginLanguage();
+
+ protected abstract void validate() throws PluginConfigException;
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultPluginConfig that = (DefaultPluginConfig) o;
+
+ if (pluginName != null ? !pluginName.equals(that.pluginName) : that.pluginName != null) {
+ return false;
+ }
+ if (pluginType != that.pluginType) {
+ return false;
+ }
+ if (pluginVersionRoot != null ? !pluginVersionRoot.equals(that.pluginVersionRoot) : that.pluginVersionRoot != null) {
+ return false;
+ }
+ if (version != null ? !version.equals(that.version) : that.version != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = pluginName != null ? pluginName.hashCode() : 0;
+ result = 31 * result + (pluginType != null ? pluginType.hashCode() : 0);
+ result = 31 * result + (version != null ? version.hashCode() : 0);
+ result = 31 * result + (pluginVersionRoot != null ? pluginVersionRoot.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultPluginConfig");
+ sb.append("{pluginName='").append(pluginName).append('\'');
+ sb.append(", pluginType=").append(pluginType);
+ sb.append(", version='").append(version).append('\'');
+ sb.append(", pluginVersionRoot=").append(pluginVersionRoot);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginConfigServiceApi.java b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginConfigServiceApi.java
new file mode 100644
index 0000000..331ef68
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginConfigServiceApi.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.pluginconf;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.killbill.billing.osgi.api.config.PluginConfigServiceApi;
+import org.killbill.billing.osgi.api.config.PluginJavaConfig;
+import org.killbill.billing.osgi.api.config.PluginRubyConfig;
+
+public class DefaultPluginConfigServiceApi implements PluginConfigServiceApi {
+
+ private final Map<Long, PluginJavaConfig> javaConfigMappings = new HashMap<Long, PluginJavaConfig>();
+ private final Map<Long, PluginRubyConfig> rubyConfigMappings = new HashMap<Long, PluginRubyConfig>();
+
+ @Override
+ public PluginJavaConfig getPluginJavaConfig(final long bundleId) {
+ synchronized (javaConfigMappings) {
+ return javaConfigMappings.get(bundleId);
+ }
+ }
+
+ @Override
+ public PluginRubyConfig getPluginRubyConfig(final long bundleId) {
+ synchronized (rubyConfigMappings) {
+ return rubyConfigMappings.get(bundleId);
+ }
+ }
+
+ public void registerBundle(final Long bundleId, final PluginJavaConfig javaConfig) {
+ synchronized (javaConfigMappings) {
+ javaConfigMappings.put(bundleId, javaConfig);
+ }
+ }
+
+ public void registerBundle(final Long bundleId, final PluginRubyConfig rubyConfig) {
+ synchronized (rubyConfigMappings) {
+ rubyConfigMappings.put(bundleId, rubyConfig);
+ }
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginJavaConfig.java b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginJavaConfig.java
new file mode 100644
index 0000000..1bb4dfc
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginJavaConfig.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.pluginconf;
+
+import java.io.File;
+import java.util.Properties;
+
+import org.killbill.billing.osgi.api.config.PluginJavaConfig;
+
+public class DefaultPluginJavaConfig extends DefaultPluginConfig implements PluginJavaConfig {
+
+ private final String bundleJarPath;
+
+ public DefaultPluginJavaConfig(final String pluginName, final String version, final File pluginVersionRoot, final Properties props) throws PluginConfigException {
+ super(pluginName, version, props, pluginVersionRoot);
+ this.bundleJarPath = extractJarPath(pluginVersionRoot);
+ validate();
+ }
+
+ private String extractJarPath(final File pluginVersionRoot) {
+ final File[] files = pluginVersionRoot.listFiles();
+ if (files == null) {
+ return null;
+ }
+
+ for (final File f : files) {
+ if (f.isFile() && f.getName().endsWith(".jar")) {
+ return f.getAbsolutePath();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getBundleJarPath() {
+ return bundleJarPath;
+ }
+
+ @Override
+ public PluginLanguage getPluginLanguage() {
+ return PluginLanguage.JAVA;
+ }
+
+ @Override
+ protected void validate() throws PluginConfigException {
+ if (bundleJarPath == null) {
+ throw new PluginConfigException("Invalid plugin " + getPluginVersionnedName() + ": cannot find jar file");
+ }
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginRubyConfig.java b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginRubyConfig.java
new file mode 100644
index 0000000..9603341
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/DefaultPluginRubyConfig.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.pluginconf;
+
+import java.io.File;
+import java.util.Properties;
+
+import org.killbill.billing.osgi.api.config.PluginRubyConfig;
+
+public class DefaultPluginRubyConfig extends DefaultPluginConfig implements PluginRubyConfig {
+
+ private static final String INSTALLATION_GEM_NAME = "gems";
+
+ private static final String PROP_RUBY_MAIN_CLASS_NAME = "mainClass";
+ private static final String PROP_RUBY_REQUIRE = "require";
+
+ private final String rubyMainClass;
+ private final File rubyLoadDir;
+ private final String rubyRequire;
+
+ public DefaultPluginRubyConfig(final String pluginName, final String version, final File pluginVersionRoot, final Properties props) throws PluginConfigException {
+ super(pluginName, version, props, pluginVersionRoot);
+ this.rubyMainClass = props.getProperty(PROP_RUBY_MAIN_CLASS_NAME);
+ this.rubyLoadDir = new File(pluginVersionRoot.getAbsolutePath() + "/" + INSTALLATION_GEM_NAME);
+ this.rubyRequire = props.getProperty(PROP_RUBY_REQUIRE);
+ validate();
+ }
+
+ @Override
+ protected void validate() throws PluginConfigException {
+ if (rubyMainClass == null) {
+ throw new PluginConfigException("Missing property " + PROP_RUBY_MAIN_CLASS_NAME + " for plugin " + getPluginVersionnedName());
+ }
+ if (rubyLoadDir == null || !rubyLoadDir.exists() || !rubyLoadDir.isDirectory()) {
+ throw new PluginConfigException("Missing gem installation directory " + rubyLoadDir.getAbsolutePath() + " for plugin " + getPluginVersionnedName());
+ }
+ }
+
+ @Override
+ public String getRubyMainClass() {
+ return rubyMainClass;
+ }
+
+ @Override
+ public String getRubyLoadDir() {
+ return rubyLoadDir.getAbsolutePath();
+ }
+
+ @Override
+ public String getRubyRequire() {
+ return rubyRequire;
+ }
+
+ @Override
+ public PluginLanguage getPluginLanguage() {
+ return PluginLanguage.RUBY;
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/PluginConfigException.java b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/PluginConfigException.java
new file mode 100644
index 0000000..e1c7bb8
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/PluginConfigException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.pluginconf;
+
+import java.io.IOException;
+
+public class PluginConfigException extends Exception {
+
+ public PluginConfigException(final String msg) {
+ super(msg);
+ }
+
+ public PluginConfigException(final String msg, final IOException ioe) {
+ super(msg, ioe);
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/PluginFinder.java b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/PluginFinder.java
new file mode 100644
index 0000000..25eb047
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/PluginFinder.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.pluginconf;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.osgi.api.config.PluginConfig;
+import org.killbill.billing.osgi.api.config.PluginConfig.PluginLanguage;
+import org.killbill.billing.osgi.api.config.PluginJavaConfig;
+import org.killbill.billing.osgi.api.config.PluginRubyConfig;
+import org.killbill.billing.util.config.OSGIConfig;
+
+public class PluginFinder {
+
+
+ private final Logger logger = LoggerFactory.getLogger(PluginFinder.class);
+
+ private final OSGIConfig osgiConfig;
+ private final Map<String, List<? extends PluginConfig>> allPlugins;
+
+ @Inject
+ public PluginFinder(final OSGIConfig osgiConfig) {
+ this.osgiConfig = osgiConfig;
+ this.allPlugins = new HashMap<String, List<? extends PluginConfig>>();
+ }
+
+ public List<PluginJavaConfig> getLatestJavaPlugins() throws PluginConfigException {
+ return getLatestPluginForLanguage(PluginLanguage.JAVA);
+ }
+
+ public List<PluginRubyConfig> getLatestRubyPlugins() throws PluginConfigException {
+ return getLatestPluginForLanguage(PluginLanguage.RUBY);
+ }
+
+ public <T extends PluginConfig> List<T> getVersionsForPlugin(final String lookupName) throws PluginConfigException {
+ loadPluginsIfRequired();
+
+ final List<T> result = new LinkedList<T>();
+ for (final String pluginName : allPlugins.keySet()) {
+ if (pluginName.equals(lookupName)) {
+ for (final PluginConfig cur : allPlugins.get(pluginName)) {
+ result.add((T) cur);
+ }
+ }
+ }
+ return result;
+ }
+
+ private <T extends PluginConfig> List<T> getLatestPluginForLanguage(final PluginLanguage pluginLanguage) throws PluginConfigException {
+ loadPluginsIfRequired();
+
+ final List<T> result = new LinkedList<T>();
+ for (final String pluginName : allPlugins.keySet()) {
+ final T plugin = (T) allPlugins.get(pluginName).get(0);
+ if (pluginLanguage != plugin.getPluginLanguage()) {
+ continue;
+ }
+ result.add(plugin);
+ }
+
+ return result;
+ }
+
+ private void loadPluginsIfRequired() throws PluginConfigException {
+ synchronized (allPlugins) {
+
+ if (allPlugins.size() > 0) {
+ return;
+ }
+
+ loadPluginsForLanguage(PluginLanguage.RUBY);
+ loadPluginsForLanguage(PluginLanguage.JAVA);
+
+ // Order for each plugin by versions starting from highest version
+ for (final String pluginName : allPlugins.keySet()) {
+ final List<? extends PluginConfig> value = allPlugins.get(pluginName);
+ Collections.sort(value, new Comparator<PluginConfig>() {
+ @Override
+ public int compare(final PluginConfig o1, final PluginConfig o2) {
+ return -(o1.getVersion().compareTo(o2.getVersion()));
+ }
+ });
+ }
+ }
+ }
+
+ private <T extends PluginConfig> void loadPluginsForLanguage(final PluginLanguage pluginLanguage) throws PluginConfigException {
+ final String rootDirPath = osgiConfig.getRootInstallationDir() + "/plugins/" + pluginLanguage.toString().toLowerCase();
+ final File rootDir = new File(rootDirPath);
+ if (!rootDir.exists() || !rootDir.isDirectory()) {
+ logger.warn("Configuration root dir {} is not a valid directory", rootDirPath);
+ return;
+ }
+
+ final File[] files = rootDir.listFiles();
+ if (files == null) {
+ return;
+ }
+ for (final File curPlugin : files) {
+ // Skip any non directory entry
+ if (!curPlugin.isDirectory()) {
+ logger.warn("Skipping entry {} in directory {}", curPlugin.getName(), rootDir.getAbsolutePath());
+ continue;
+ }
+ final String pluginName = curPlugin.getName();
+
+ final File[] filesInDir = curPlugin.listFiles();
+ if (filesInDir == null) {
+ continue;
+ }
+ for (final File curVersion : filesInDir) {
+ // Skip any non directory entry
+ if (!curVersion.isDirectory()) {
+ logger.warn("Skipping entry {} in directory {}", curPlugin.getName(), rootDir.getAbsolutePath());
+ continue;
+ }
+ final String version = curVersion.getName();
+
+ final T plugin = extractPluginConfig(pluginLanguage, pluginName, version, curVersion);
+ List<T> curPluginVersionlist = (List<T>) allPlugins.get(plugin.getPluginName());
+ if (curPluginVersionlist == null) {
+ curPluginVersionlist = new LinkedList<T>();
+ allPlugins.put(plugin.getPluginName(), curPluginVersionlist);
+ }
+ curPluginVersionlist.add(plugin);
+ logger.info("Adding plugin {} ", plugin.getPluginVersionnedName());
+ }
+ }
+ }
+
+ private <T extends PluginConfig> T extractPluginConfig(final PluginLanguage pluginLanguage, final String pluginName, final String pluginVersion, final File pluginVersionDir) throws PluginConfigException {
+ T result;
+ Properties props = null;
+ try {
+ final File[] files = pluginVersionDir.listFiles();
+ if (files == null) {
+ throw new PluginConfigException("Unable to list files in " + pluginVersionDir.getAbsolutePath());
+ }
+
+ for (final File cur : files) {
+ if (cur.isFile() && cur.getName().equals(osgiConfig.getOSGIKillbillPropertyName())) {
+ props = readPluginConfigurationFile(cur);
+ }
+ if (props != null) {
+ break;
+ }
+ }
+
+ if (pluginLanguage == PluginLanguage.RUBY && props == null) {
+ throw new PluginConfigException("Invalid plugin configuration file for " + pluginName + "-" + pluginVersion);
+ }
+
+ } catch (IOException e) {
+ throw new PluginConfigException("Failed to read property file for " + pluginName + "-" + pluginVersion, e);
+ }
+ switch (pluginLanguage) {
+ case RUBY:
+ result = (T) new DefaultPluginRubyConfig(pluginName, pluginVersion, pluginVersionDir, props);
+ break;
+ case JAVA:
+ result = (T) new DefaultPluginJavaConfig(pluginName, pluginVersion, pluginVersionDir, (props == null) ? new Properties() : props);
+ break;
+ default:
+ throw new RuntimeException("Unknown plugin language " + pluginLanguage);
+ }
+ return result;
+ }
+
+ private Properties readPluginConfigurationFile(final File config) throws IOException {
+ final Properties props = new Properties();
+ final BufferedReader br = new BufferedReader(new FileReader(config));
+ String line;
+ while ((line = br.readLine()) != null) {
+ final String[] parts = line.split("\\s*=\\s*");
+ final String key = parts[0];
+ final String value = parts[1];
+ props.put(key, value);
+ }
+ br.close();
+ return props;
+ }
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/PuginConfServiceApi.java b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/PuginConfServiceApi.java
new file mode 100644
index 0000000..51bd300
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/pluginconf/PuginConfServiceApi.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.pluginconf;
+
+public class PuginConfServiceApi {
+
+}
diff --git a/osgi/src/main/java/org/killbill/billing/osgi/PureOSGIBundleFinder.java b/osgi/src/main/java/org/killbill/billing/osgi/PureOSGIBundleFinder.java
new file mode 100644
index 0000000..1efe1e6
--- /dev/null
+++ b/osgi/src/main/java/org/killbill/billing/osgi/PureOSGIBundleFinder.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.osgi.pluginconf.PluginConfigException;
+import org.killbill.billing.util.config.OSGIConfig;
+
+import com.google.common.collect.ImmutableList;
+
+@Singleton
+public class PureOSGIBundleFinder {
+
+ private final Logger logger = LoggerFactory.getLogger(Logger.class);
+
+ private final OSGIConfig osgiConfig;
+
+ @Inject
+ public PureOSGIBundleFinder(final OSGIConfig osgiConfig) {
+ this.osgiConfig = osgiConfig;
+ }
+
+ public List<String> getLatestBundles() throws PluginConfigException {
+ final String rootDirPath = getPlatformOSGIBundlesRootDir();
+ final File rootDir = new File(rootDirPath);
+ if (!rootDir.exists() || !rootDir.isDirectory()) {
+ logger.warn("Configuration root dir {} is not a valid directory", rootDirPath);
+ return ImmutableList.<String>of();
+ }
+
+ final File[] files = rootDir.listFiles();
+ if (files == null) {
+ return ImmutableList.<String>of();
+ }
+
+ final List<String> bundles = new ArrayList<String>();
+ for (final File bundleJar : files) {
+ if (bundleJar.isFile()) {
+ bundles.add(bundleJar.getAbsolutePath());
+ }
+ }
+
+ return bundles;
+ }
+
+ public String getPlatformOSGIBundlesRootDir() {
+ return osgiConfig.getRootInstallationDir() + "/platform/";
+ }
+}
diff --git a/osgi/src/test/java/org/killbill/billing/osgi/TestKillbillActivator.java b/osgi/src/test/java/org/killbill/billing/osgi/TestKillbillActivator.java
new file mode 100644
index 0000000..317c457
--- /dev/null
+++ b/osgi/src/test/java/org/killbill/billing/osgi/TestKillbillActivator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi;
+
+import java.util.regex.Matcher;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+
+public class TestKillbillActivator extends GuicyKillbillTestSuiteNoDB {
+
+ @Test(groups= "fast")
+ public void testPluginNamePatternGood() {
+ Matcher m = KillbillActivator.PLUGIN_NAME_PATTERN.matcher("a");
+ Assert.assertTrue(m.matches());
+
+ m = KillbillActivator.PLUGIN_NAME_PATTERN.matcher("abc1223");
+ Assert.assertTrue(m.matches());
+
+ m = KillbillActivator.PLUGIN_NAME_PATTERN.matcher("abc123-");
+ Assert.assertTrue(m.matches());
+
+ m = KillbillActivator.PLUGIN_NAME_PATTERN.matcher("abc123-zs");
+ Assert.assertTrue(m.matches());
+
+ m = KillbillActivator.PLUGIN_NAME_PATTERN.matcher("xyz_1");
+ Assert.assertTrue(m.matches());
+
+ m = KillbillActivator.PLUGIN_NAME_PATTERN.matcher("osgi-payment-plugin");
+ Assert.assertTrue(m.matches());
+ }
+
+
+ @Test(groups= "fast")
+ public void testPluginNamePatternBad() {
+ Matcher m = KillbillActivator.PLUGIN_NAME_PATTERN.matcher("1abd");
+ Assert.assertFalse(m.matches());
+
+ m = KillbillActivator.PLUGIN_NAME_PATTERN.matcher("Tata");
+ Assert.assertFalse(m.matches());
+
+
+ m = KillbillActivator.PLUGIN_NAME_PATTERN.matcher("Tata#");
+ Assert.assertFalse(m.matches());
+
+ m = KillbillActivator.PLUGIN_NAME_PATTERN.matcher("yo:");
+ Assert.assertFalse(m.matches());
+ }
+
+ @Test(groups = "false")
+ public void testPluginNameLength() {
+
+ String pluginNameGood = "foofofoSuperFoo";
+ Assert.assertTrue(pluginNameGood.length() < KillbillActivator.PLUGIN_NAME_MAX_LENGTH);
+
+ String pluginNameBAd = "foofoofooSuperFoosupersuperLongreallyLong";
+ Assert.assertFalse(pluginNameBAd.length() < KillbillActivator.PLUGIN_NAME_MAX_LENGTH);
+ }
+}
osgi-bundles/bundles/jruby/pom.xml 86(+43 -43)
diff --git a/osgi-bundles/bundles/jruby/pom.xml b/osgi-bundles/bundles/jruby/pom.xml
index 3eba762..02f451a 100644
--- a/osgi-bundles/bundles/jruby/pom.xml
+++ b/osgi-bundles/bundles/jruby/pom.xml
@@ -18,9 +18,9 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-osgi-bundles</artifactId>
- <version>0.9.0-SNAPSHOT</version>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-bundles-jruby</artifactId>
@@ -32,30 +32,30 @@
<artifactId>guava</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
<!--
<scope>provided</scope>
-->
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-notification</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-payment</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-currency</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-osgi-bundles-lib-killbill</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-concurrent</artifactId>
</dependency>
<dependency>
@@ -124,44 +124,44 @@
<extensions>true</extensions>
<configuration>
<instructions>
- <Bundle-Activator>com.ning.billing.osgi.bundles.jruby.JRubyActivator</Bundle-Activator>
+ <Bundle-Activator>org.killbill.billing.osgi.bundles.jruby.JRubyActivator</Bundle-Activator>
<Export-Package />
- <Private-Package>com.ning.billing.osgi.bundles.jruby.*</Private-Package>
+ <Private-Package>org.killbill.billing.osgi.bundles.jruby.*</Private-Package>
<!-- Optional resolution because exported by the Felix system bundle -->
<Import-Package>*;resolution:=optional,
- com.ning.billing.account.api;
- com.ning.billing.analytics.api.sanity;
- com.ning.billing.analytics.api.user;
- com.ning.billing.beatrix.bus.api;
- com.ning.billing.catalog.api;
- com.ning.billing.subscription.api;
- com.ning.billing.subscription.api.migration;
- com.ning.billing.subscription.api.timeline;
- com.ning.billing.subscription.api.transfer;
- com.ning.billing.subscription.api.user;
- com.ning.billing.entitlement.api;
- com.ning.billing.invoice.api;
- com.ning.billing.junction.api;
- com.ning.billing;
- com.ning.billing.osgi.api;
- com.ning.billing.osgi.api.config;
- com.ning.billing.overdue;
- com.ning.billing.payment.api;
- com.ning.billing.payment.plugin.api;
- com.ning.billing.currency.plugin.api;
- com.ning.billing.tenant.api;
- com.ning.billing.usage.api;
- com.ning.billing.util.api;
- com.ning.billing.util.audit;
- com.ning.billing.util.callcontext;
- com.ning.billing.util.customfield;
- com.ning.billing.notification.plugin;
- com.ning.billing.currency.api;
- com.ning.billing.util.email;
- com.ning.billing.util.entity;
- com.ning.billing.util.tag;
- com.ning.billing.util.template;
- com.ning.billing.util.template.translation;resolution:=optional,
+ org.killbill.billing.account.api;
+ org.killbill.billing.analytics.api.sanity;
+ org.killbill.billing.analytics.api.user;
+ org.killbill.billing.beatrix.bus.api;
+ org.killbill.billing.catalog.api;
+ org.killbill.billing.subscription.api;
+ org.killbill.billing.subscription.api.migration;
+ org.killbill.billing.subscription.api.timeline;
+ org.killbill.billing.subscription.api.transfer;
+ org.killbill.billing.subscription.api.user;
+ org.killbill.billing.entitlement.api;
+ org.killbill.billing.invoice.api;
+ org.killbill.billing.junction.api;
+ org.killbill.billing;
+ org.killbill.billing.osgi.api;
+ org.killbill.billing.osgi.api.config;
+ org.killbill.billing.overdue;
+ org.killbill.billing.payment.api;
+ org.killbill.billing.payment.plugin.api;
+ org.killbill.billing.currency.plugin.api;
+ org.killbill.billing.tenant.api;
+ org.killbill.billing.usage.api;
+ org.killbill.billing.util.api;
+ org.killbill.billing.util.audit;
+ org.killbill.billing.util.callcontext;
+ org.killbill.billing.util.customfield;
+ org.killbill.billing.notification.plugin;
+ org.killbill.billing.currency.api;
+ org.killbill.billing.util.email;
+ org.killbill.billing.util.entity;
+ org.killbill.billing.util.tag;
+ org.killbill.billing.util.template;
+ org.killbill.billing.util.template.translation;resolution:=optional,
org.joda.time;org.joda.time.format;resolution:=optional,
sun.misc;
sun.misc.*;
diff --git a/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyActivator.java b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyActivator.java
new file mode 100644
index 0000000..569e265
--- /dev/null
+++ b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyActivator.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.jruby;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.log.LogService;
+
+import org.killbill.commons.concurrent.Executors;
+import org.killbill.billing.osgi.api.config.PluginConfig.PluginType;
+import org.killbill.billing.osgi.api.config.PluginConfigServiceApi;
+import org.killbill.billing.osgi.api.config.PluginRubyConfig;
+import org.killbill.killbill.osgi.libs.killbill.KillbillActivatorBase;
+import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillEventDispatcher.OSGIKillbillEventHandler;
+
+import com.google.common.base.Objects;
+
+public class JRubyActivator extends KillbillActivatorBase {
+
+ private static final String JRUBY_PLUGINS_CONF_DIR = System.getProperty("org.killbill.billing.osgi.bundles.jruby.conf.dir");
+ private static final int JRUBY_PLUGINS_RESTART_DELAY_SECS = Integer.parseInt(System.getProperty("org.killbill.billing.osgi.bundles.jruby.restart.delay.secs", "5"));
+
+ private static final String TMP_DIR_NAME = "tmp";
+ private static final String RESTART_FILE_NAME = "restart.txt";
+
+ private JRubyPlugin plugin = null;
+ private ScheduledFuture<?> restartFuture = null;
+
+ private static final String KILLBILL_PLUGIN_JPAYMENT = "Killbill::Plugin::Api::PaymentPluginApi";
+ private static final String KILLBILL_PLUGIN_JNOTIFICATION = "Killbill::Plugin::Api::NotificationPluginApi";
+ private static final String KILLBILL_PLUGIN_JCURRENCY = "Killbill::Plugin::Api::CurrencyPluginApi";
+
+ public void start(final BundleContext context) throws Exception {
+ super.start(context);
+
+ withContextClassLoader(new PluginCall() {
+ @Override
+ public void doCall() {
+ logService.log(LogService.LOG_INFO, "JRuby bundle activated");
+
+ // Retrieve the plugin config
+ final PluginRubyConfig rubyConfig = retrievePluginRubyConfig(context);
+
+ // Setup JRuby
+ final String pluginMain;
+ if (PluginType.NOTIFICATION.equals(rubyConfig.getPluginType())) {
+ plugin = new JRubyNotificationPlugin(rubyConfig, context, logService);
+ dispatcher.registerEventHandler((OSGIKillbillEventHandler) plugin);
+ pluginMain = KILLBILL_PLUGIN_JNOTIFICATION;
+ } else if (PluginType.PAYMENT.equals(rubyConfig.getPluginType())) {
+ plugin = new JRubyPaymentPlugin(rubyConfig, context, logService);
+ pluginMain = KILLBILL_PLUGIN_JPAYMENT;
+ } else if (PluginType.CURRENCY.equals(rubyConfig.getPluginType())) {
+ plugin = new JRubyCurrencyPlugin(rubyConfig, context, logService);
+ pluginMain = KILLBILL_PLUGIN_JCURRENCY;
+ } else {
+ throw new IllegalStateException("Unsupported plugin type " + rubyConfig.getPluginType());
+ }
+
+ // Validate and instantiate the plugin
+ startPlugin(rubyConfig, pluginMain, context);
+ }
+ }, this.getClass().getClassLoader());
+ }
+
+ private void startPlugin(final PluginRubyConfig rubyConfig, final String pluginMain, final BundleContext context) {
+ final Map<String, Object> killbillServices = retrieveKillbillApis(context);
+ killbillServices.put("root", rubyConfig.getPluginVersionRoot().getAbsolutePath());
+ killbillServices.put("logger", logService);
+ // Default to the plugin root dir if no jruby plugins specific configuration directory was specified
+ killbillServices.put("conf_dir", Objects.firstNonNull(JRUBY_PLUGINS_CONF_DIR, rubyConfig.getPluginVersionRoot().getAbsolutePath()));
+
+ // Setup the restart mechanism. This is useful for hotswapping plugin code
+ // The principle is similar to the one in Phusion Passenger:
+ // http://www.modrails.com/documentation/Users%20guide%20Apache.html#_redeploying_restarting_the_ruby_on_rails_application
+ final File tmpDirPath = new File(rubyConfig.getPluginVersionRoot().getAbsolutePath() + "/" + TMP_DIR_NAME);
+ if (!tmpDirPath.exists()) {
+ if (!tmpDirPath.mkdir()) {
+ logService.log(LogService.LOG_WARNING, "Unable to create directory " + tmpDirPath + ", the restart mechanism is disabled");
+ return;
+ }
+ }
+ if (!tmpDirPath.isDirectory()) {
+ logService.log(LogService.LOG_WARNING, tmpDirPath + " is not a directory, the restart mechanism is disabled");
+ return;
+ }
+ // Start the plugin synchronously and schedule the restart logic
+ doStartPlugin(pluginMain, context, killbillServices);
+
+ restartFuture = Executors.newSingleThreadScheduledExecutor("jruby-restarter-" + pluginMain)
+ .scheduleWithFixedDelay(new Runnable() {
+ long lastRestartMillis = System.currentTimeMillis();
+
+ @Override
+ public void run() {
+
+ final File restartFile = new File(tmpDirPath + "/" + RESTART_FILE_NAME);
+ if (!restartFile.isFile()) {
+ return;
+ }
+
+ if (restartFile.lastModified() > lastRestartMillis) {
+ logService.log(LogService.LOG_INFO, "Restarting JRuby plugin " + rubyConfig.getRubyMainClass());
+
+ doStopPlugin(context);
+ doStartPlugin(pluginMain, context, killbillServices);
+
+ lastRestartMillis = restartFile.lastModified();
+ }
+ }
+ }, JRUBY_PLUGINS_RESTART_DELAY_SECS, JRUBY_PLUGINS_RESTART_DELAY_SECS, TimeUnit.SECONDS);
+ }
+
+ private PluginRubyConfig retrievePluginRubyConfig(final BundleContext context) {
+ final PluginConfigServiceApi pluginConfigServiceApi = killbillAPI.getPluginConfigServiceApi();
+ return pluginConfigServiceApi.getPluginRubyConfig(context.getBundle().getBundleId());
+ }
+
+ public void stop(final BundleContext context) throws Exception {
+
+ withContextClassLoader(new PluginCall() {
+ @Override
+ public void doCall() {
+ restartFuture.cancel(true);
+ doStopPlugin(context);
+ killbillAPI.close();
+ logService.close();
+ }
+ }, this.getClass().getClassLoader());
+ }
+
+ private void doStartPlugin(final String pluginMain, final BundleContext context, final Map<String, Object> killbillServices) {
+ logService.log(LogService.LOG_INFO, "Starting JRuby plugin " + pluginMain);
+ plugin.instantiatePlugin(killbillServices, pluginMain);
+ plugin.startPlugin(context);
+ logService.log(LogService.LOG_INFO, "JRuby plugin " + pluginMain + " started");
+ }
+
+ private void doStopPlugin(final BundleContext context) {
+ logService.log(LogService.LOG_INFO, "Stopping JRuby plugin " + context.getBundle().getSymbolicName());
+ plugin.stopPlugin(context);
+ plugin.unInstantiatePlugin();
+ logService.log(LogService.LOG_INFO, "Stopped JRuby plugin " + context.getBundle().getSymbolicName());
+ }
+
+ // We make the explicit registration in the start method by hand as this would be called too early
+ // (see OSGIKillbillEventDispatcher)
+ @Override
+ public OSGIKillbillEventHandler getOSGIKillbillEventHandler() {
+ return null;
+ }
+
+ private Map<String, Object> retrieveKillbillApis(final BundleContext context) {
+ final Map<String, Object> killbillUserApis = new HashMap<String, Object>();
+
+ // See killbill/plugin.rb for the naming convention magic
+ killbillUserApis.put("account_user_api", killbillAPI.getAccountUserApi());
+ killbillUserApis.put("catalog_user_api", killbillAPI.getCatalogUserApi());
+ killbillUserApis.put("invoice_payment_api", killbillAPI.getInvoicePaymentApi());
+ killbillUserApis.put("invoice_user_api", killbillAPI.getInvoiceUserApi());
+ killbillUserApis.put("subscription_api", killbillAPI.getSubscriptionApi());
+ killbillUserApis.put("entitlement_api", killbillAPI.getEntitlementApi());
+ killbillUserApis.put("payment_api", killbillAPI.getPaymentApi());
+ killbillUserApis.put("custom_field_user_api", killbillAPI.getCustomFieldUserApi());
+ killbillUserApis.put("tag_user_api", killbillAPI.getTagUserApi());
+ killbillUserApis.put("currency_conversion_api", killbillAPI.getCurrencyConversionApi());
+ return killbillUserApis;
+ }
+
+
+ private static interface PluginCall {
+
+ public void doCall();
+ }
+
+ // JRuby/Felix specifics, it works out of the box on Equinox.
+ // Other OSGI frameworks are untested.
+ private void withContextClassLoader(final PluginCall call, final ClassLoader pluginClassLoader) {
+ final ClassLoader enteringContextClassLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(pluginClassLoader);
+ call.doCall();
+ } finally {
+ // We want to make sure that calling thread gets back its original callcontext class loader when it returns
+ Thread.currentThread().setContextClassLoader(enteringContextClassLoader);
+ }
+ }
+}
diff --git a/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyCurrencyPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyCurrencyPlugin.java
new file mode 100644
index 0000000..4c22c32
--- /dev/null
+++ b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyCurrencyPlugin.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.jruby;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Set;
+import java.util.SortedSet;
+
+import org.joda.time.DateTime;
+import org.jruby.Ruby;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.log.LogService;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.currency.api.Rate;
+import org.killbill.billing.currency.plugin.api.CurrencyPluginApi;
+import org.killbill.billing.osgi.api.OSGIPluginProperties;
+import org.killbill.billing.osgi.api.config.PluginRubyConfig;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+
+public class JRubyCurrencyPlugin extends JRubyPlugin implements CurrencyPluginApi {
+
+ private volatile ServiceRegistration<CurrencyPluginApi> currencyPluginRegistration;
+
+ public JRubyCurrencyPlugin(final PluginRubyConfig config, final BundleContext bundleContext, final LogService logger) {
+ super(config, bundleContext, logger);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void startPlugin(final BundleContext context) {
+ super.startPlugin(context);
+
+ final Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put("name", pluginMainClass);
+ props.put(OSGIPluginProperties.PLUGIN_NAME_PROP, pluginGemName);
+ currencyPluginRegistration = (ServiceRegistration<CurrencyPluginApi>) context.registerService(CurrencyPluginApi.class.getName(), this, props);
+ }
+
+ @Override
+ public void stopPlugin(final BundleContext context) {
+ if (currencyPluginRegistration != null) {
+ currencyPluginRegistration.unregister();
+ }
+ super.stopPlugin(context);
+ }
+
+ @Override
+ public Set<Currency> getBaseCurrencies() {
+ try {
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.CURRENCY) {
+ @Override
+ public Set<Currency> doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((CurrencyPluginApi) pluginInstance).getBaseCurrencies();
+ }
+ });
+ } catch (PaymentPluginApiException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public DateTime getLatestConversionDate(final Currency currency) {
+ try {
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.CURRENCY) {
+ @Override
+ public DateTime doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((CurrencyPluginApi) pluginInstance).getLatestConversionDate(currency);
+ }
+ });
+ } catch (PaymentPluginApiException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public SortedSet<DateTime> getConversionDates(final Currency currency) {
+ try {
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.CURRENCY) {
+ @Override
+ public SortedSet<DateTime> doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((CurrencyPluginApi) pluginInstance).getConversionDates(currency);
+ }
+ });
+ } catch (PaymentPluginApiException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Set<Rate> getCurrentRates(final Currency currency) {
+ try {
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.CURRENCY) {
+ @Override
+ public Set<Rate> doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((CurrencyPluginApi) pluginInstance).getCurrentRates(currency);
+ }
+ });
+ } catch (PaymentPluginApiException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Set<Rate> getRates(final Currency currency, final DateTime time) {
+ try {
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.CURRENCY) {
+ @Override
+ public Set<Rate> doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((CurrencyPluginApi) pluginInstance).getRates(currency, time);
+ }
+ });
+ } catch (PaymentPluginApiException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyHttpServlet.java b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyHttpServlet.java
new file mode 100644
index 0000000..b069099
--- /dev/null
+++ b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyHttpServlet.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.jruby;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.jruby.runtime.builtin.IRubyObject;
+
+public class JRubyHttpServlet extends HttpServlet {
+
+ private final HttpServlet delegate;
+
+ public JRubyHttpServlet(final IRubyObject rubyObject) {
+ delegate = (HttpServlet) rubyObject.toJava(HttpServlet.class);
+ }
+
+ @Override
+ protected void service(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ delegate.service(req, resp);
+ }
+}
diff --git a/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java
new file mode 100644
index 0000000..717e057
--- /dev/null
+++ b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyNotificationPlugin.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.jruby;
+
+import org.jruby.Ruby;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.log.LogService;
+
+import org.killbill.billing.notification.plugin.api.ExtBusEvent;
+import org.killbill.billing.notification.plugin.api.NotificationPluginApi;
+import org.killbill.billing.osgi.api.config.PluginRubyConfig;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillEventDispatcher.OSGIKillbillEventHandler;
+
+public class JRubyNotificationPlugin extends JRubyPlugin implements OSGIKillbillEventHandler {
+
+ public JRubyNotificationPlugin(final PluginRubyConfig config, final BundleContext bundleContext, final LogService logger) {
+ super(config, bundleContext, logger);
+ }
+
+ @Override
+ public void handleKillbillEvent(final ExtBusEvent killbillEvent) {
+ try {
+ callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.NOTIFICATION) {
+ @Override
+ public Void doCall(final Ruby runtime) throws PaymentPluginApiException {
+ ((NotificationPluginApi) pluginInstance).onEvent(killbillEvent);
+ return null;
+ }
+ });
+ } catch (PaymentPluginApiException e) {
+ throw new IllegalStateException("Unexpected PaymentApiException for notification plugin", e);
+ }
+ }
+}
diff --git a/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
new file mode 100644
index 0000000..e6bf8ca
--- /dev/null
+++ b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.jruby;
+
+import java.math.BigDecimal;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.UUID;
+
+import org.jruby.Ruby;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.log.LogService;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.osgi.api.OSGIPluginProperties;
+import org.killbill.billing.osgi.api.config.PluginRubyConfig;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.billing.payment.plugin.api.RefundInfoPlugin;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+
+public class JRubyPaymentPlugin extends JRubyPlugin implements PaymentPluginApi {
+
+ private volatile ServiceRegistration<PaymentPluginApi> paymentInfoPluginRegistration;
+
+ public JRubyPaymentPlugin(final PluginRubyConfig config, final BundleContext bundleContext, final LogService logger) {
+ super(config, bundleContext, logger);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void startPlugin(final BundleContext context) {
+ super.startPlugin(context);
+
+ final Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put("name", pluginMainClass);
+ props.put(OSGIPluginProperties.PLUGIN_NAME_PROP, pluginGemName);
+ paymentInfoPluginRegistration = (ServiceRegistration<PaymentPluginApi>) context.registerService(PaymentPluginApi.class.getName(), this, props);
+ }
+
+ @Override
+ public void stopPlugin(final BundleContext context) {
+ if (paymentInfoPluginRegistration != null) {
+ paymentInfoPluginRegistration.unregister();
+ }
+ super.stopPlugin(context);
+ }
+
+ @Override
+ public PaymentInfoPlugin processPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public PaymentInfoPlugin doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((PaymentPluginApi) pluginInstance).processPayment(kbAccountId, kbPaymentId, kbPaymentMethodId, amount, currency, context);
+ }
+ });
+ }
+
+ @Override
+ public PaymentInfoPlugin getPaymentInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) throws PaymentPluginApiException {
+
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public PaymentInfoPlugin doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((PaymentPluginApi) pluginInstance).getPaymentInfo(kbAccountId, kbPaymentId, context);
+ }
+ });
+ }
+
+ @Override
+ public Pagination<PaymentInfoPlugin> searchPayments(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public Pagination<PaymentInfoPlugin> doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((PaymentPluginApi) pluginInstance).searchPayments(searchKey, offset, limit, tenantContext);
+ }
+ });
+ }
+
+ @Override
+ public RefundInfoPlugin processRefund(final UUID kbAccountId, final UUID kbPaymentId, final BigDecimal refundAmount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public RefundInfoPlugin doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((PaymentPluginApi) pluginInstance).processRefund(kbAccountId, kbPaymentId, refundAmount, currency, context);
+ }
+ });
+
+ }
+
+ @Override
+ public List<RefundInfoPlugin> getRefundInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) throws PaymentPluginApiException {
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public List<RefundInfoPlugin> doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((PaymentPluginApi) pluginInstance).getRefundInfo(kbAccountId, kbPaymentId, context);
+ }
+ });
+ }
+
+ @Override
+ public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public Pagination<RefundInfoPlugin> doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((PaymentPluginApi) pluginInstance).searchRefunds(searchKey, offset, limit, tenantContext);
+ }
+ });
+ }
+
+ @Override
+ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final CallContext context) throws PaymentPluginApiException {
+
+ callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public Void doCall(final Ruby runtime) throws PaymentPluginApiException {
+ ((PaymentPluginApi) pluginInstance).addPaymentMethod(kbAccountId, kbPaymentMethodId, paymentMethodProps, Boolean.valueOf(setDefault), context);
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public void deletePaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+
+ callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public Void doCall(final Ruby runtime) throws PaymentPluginApiException {
+ ((PaymentPluginApi) pluginInstance).deletePaymentMethod(kbAccountId, kbPaymentMethodId, context);
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public PaymentMethodPlugin getPaymentMethodDetail(final UUID kbAccountId, final UUID kbPaymentMethodId, final TenantContext context) throws PaymentPluginApiException {
+
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public PaymentMethodPlugin doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((PaymentPluginApi) pluginInstance).getPaymentMethodDetail(kbAccountId, kbPaymentMethodId, context);
+ }
+ });
+ }
+
+ @Override
+ public void setDefaultPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+
+ callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public Void doCall(final Ruby runtime) throws PaymentPluginApiException {
+ ((PaymentPluginApi) pluginInstance).setDefaultPaymentMethod(kbAccountId, kbPaymentMethodId, context);
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public List<PaymentMethodInfoPlugin> getPaymentMethods(final UUID kbAccountId, final boolean refreshFromGateway, final CallContext context) throws PaymentPluginApiException {
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public List<PaymentMethodInfoPlugin> doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((PaymentPluginApi) pluginInstance).getPaymentMethods(kbAccountId, Boolean.valueOf(refreshFromGateway), context);
+ }
+ });
+ }
+
+ @Override
+ public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public Pagination<PaymentMethodPlugin> doCall(final Ruby runtime) throws PaymentPluginApiException {
+ return ((PaymentPluginApi) pluginInstance).searchPaymentMethods(searchKey, offset, limit, tenantContext);
+ }
+ });
+ }
+
+ @Override
+ public void resetPaymentMethods(final UUID kbAccountId, final List<PaymentMethodInfoPlugin> paymentMethods) throws PaymentPluginApiException {
+
+ callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+ @Override
+ public Void doCall(final Ruby runtime) throws PaymentPluginApiException {
+ ((PaymentPluginApi) pluginInstance).resetPaymentMethods(kbAccountId, paymentMethods);
+ return null;
+ }
+ });
+ }
+}
diff --git a/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPlugin.java
new file mode 100644
index 0000000..06684d6
--- /dev/null
+++ b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPlugin.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.jruby;
+
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.servlet.http.HttpServlet;
+
+import org.jruby.Ruby;
+import org.jruby.RubyObject;
+import org.jruby.embed.EvalFailedException;
+import org.jruby.embed.LocalContextScope;
+import org.jruby.embed.LocalVariableBehavior;
+import org.jruby.embed.ScriptingContainer;
+import org.jruby.runtime.builtin.IRubyObject;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.log.LogService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.osgi.api.config.PluginRubyConfig;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+
+// Bridge between the OSGI bundle and the ruby plugin
+public abstract class JRubyPlugin {
+
+ private static final Logger log = LoggerFactory.getLogger(JRubyPlugin.class);
+
+ // Killbill gem base classes
+ private static final String KILLBILL_PLUGIN_BASE = "Killbill::Plugin::PluginBase";
+ private static final String KILLBILL_PLUGIN_NOTIFICATION = "Killbill::Plugin::Notification";
+ private static final String KILLBILL_PLUGIN_PAYMENT = "Killbill::Plugin::Payment";
+ private static final String KILLBILL_PLUGIN_CURRENCY = "Killbill::Plugin::Currency";
+
+ // Magic ruby variables
+ private static final String KILLBILL_SERVICES = "java_apis";
+ private static final String KILLBILL_PLUGIN_CLASS_NAME = "plugin_class_name";
+
+ // Methods implemented by Killbill::Plugin::JPlugin
+ private static final String START_PLUGIN_RUBY_METHOD_NAME = "start_plugin";
+ private static final String STOP_PLUGIN_RUBY_METHOD_NAME = "stop_plugin";
+ private static final String RACK_HANDLER_RUBY_METHOD_NAME = "rack_handler";
+
+ private final Object pluginMonitor = new Object();
+
+ protected final LogService logger;
+ protected final BundleContext bundleContext;
+ protected final String pluginGemName;
+ protected final String rubyRequire;
+ protected final String pluginMainClass;
+ protected final String pluginLibdir;
+
+ protected ScriptingContainer container;
+ protected RubyObject pluginInstance;
+
+ private ServiceRegistration httpServletServiceRegistration = null;
+ private String cachedRequireLine = null;
+
+ public JRubyPlugin(final PluginRubyConfig config, final BundleContext bundleContext, final LogService logger) {
+ this.logger = logger;
+ this.bundleContext = bundleContext;
+ this.pluginGemName = config.getPluginName();
+ this.rubyRequire = config.getRubyRequire();
+ this.pluginMainClass = config.getRubyMainClass();
+ this.pluginLibdir = config.getRubyLoadDir();
+ }
+
+ public void instantiatePlugin(final Map<String, Object> killbillApis, final String pluginMain) {
+ container = setupScriptingContainer();
+
+ checkValidPlugin();
+
+ // Register all killbill APIs
+ container.put(KILLBILL_SERVICES, killbillApis);
+ container.put(KILLBILL_PLUGIN_CLASS_NAME, pluginMainClass);
+
+ // Note that the KILLBILL_SERVICES variable will be available once only!
+ // Don't put any code here!
+
+ // Start the plugin
+ pluginInstance = (RubyObject) container.runScriptlet(pluginMain + ".new(" + KILLBILL_PLUGIN_CLASS_NAME + "," + KILLBILL_SERVICES + ")");
+ }
+
+ public synchronized void startPlugin(final BundleContext context) {
+ checkPluginIsStopped();
+ pluginInstance.callMethod(START_PLUGIN_RUBY_METHOD_NAME);
+ checkPluginIsRunning();
+ registerHttpServlet();
+ }
+
+ public synchronized void stopPlugin(final BundleContext context) {
+ checkPluginIsRunning();
+ unregisterHttpServlet();
+ pluginInstance.callMethod(STOP_PLUGIN_RUBY_METHOD_NAME);
+ checkPluginIsStopped();
+ }
+
+ public void unInstantiatePlugin() {
+ // Cleanup the container
+ container.terminate();
+ }
+
+ private void registerHttpServlet() {
+ // Register the rack handler
+ final IRubyObject rackHandler = pluginInstance.callMethod(RACK_HANDLER_RUBY_METHOD_NAME);
+ if (!rackHandler.isNil()) {
+ logger.log(LogService.LOG_INFO, String.format("Using %s as rack handler", rackHandler.getMetaClass()));
+
+ final JRubyHttpServlet jRubyHttpServlet = new JRubyHttpServlet(rackHandler);
+ final Hashtable<String, String> properties = new Hashtable<String, String>();
+ properties.put("killbill.pluginName", pluginGemName);
+ httpServletServiceRegistration = bundleContext.registerService(HttpServlet.class.getName(), jRubyHttpServlet, properties);
+ }
+ }
+
+ private void unregisterHttpServlet() {
+ if (httpServletServiceRegistration != null) {
+ httpServletServiceRegistration.unregister();
+ }
+ }
+
+ private void checkPluginIsRunning() {
+ if (pluginInstance == null || !(Boolean) pluginInstance.callMethod("is_active").toJava(Boolean.class)) {
+ throw new IllegalStateException(String.format("Plugin %s didn't start properly", pluginMainClass));
+ }
+ }
+
+ private void checkPluginIsStopped() {
+ if (pluginInstance == null || (Boolean) pluginInstance.callMethod("is_active").toJava(Boolean.class)) {
+ throw new IllegalStateException(String.format("Plugin %s didn't stop properly", pluginMainClass));
+ }
+ }
+
+ private void checkValidPlugin() {
+ try {
+ container.runScriptlet(checkInstanceOfPlugin(KILLBILL_PLUGIN_BASE));
+ } catch (EvalFailedException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private void checkValidNotificationPlugin() throws IllegalArgumentException {
+ try {
+ container.runScriptlet(checkInstanceOfPlugin(KILLBILL_PLUGIN_NOTIFICATION));
+ } catch (EvalFailedException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private void checkValidPaymentPlugin() throws IllegalArgumentException {
+ try {
+ container.runScriptlet(checkInstanceOfPlugin(KILLBILL_PLUGIN_PAYMENT));
+ } catch (EvalFailedException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private void checkValidCurrencyPlugin() throws IllegalArgumentException {
+ try {
+ container.runScriptlet(checkInstanceOfPlugin(KILLBILL_PLUGIN_CURRENCY));
+ } catch (EvalFailedException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private String checkInstanceOfPlugin(final String baseClass) {
+ final StringBuilder builder = new StringBuilder(getRequireLine());
+ builder.append("raise ArgumentError.new('Invalid plugin: ")
+ .append(pluginMainClass)
+ .append(", is not a ")
+ .append(baseClass)
+ .append("') unless ")
+ .append(pluginMainClass)
+ .append(" <= ")
+ .append(baseClass);
+ return builder.toString();
+ }
+
+ private String getRequireLine() {
+ if (cachedRequireLine == null) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("ENV[\"GEM_HOME\"] = \"").append(pluginLibdir).append("\"").append("\n");
+ builder.append("ENV[\"GEM_PATH\"] = ENV[\"GEM_HOME\"]\n");
+ // Always require the Killbill gem
+ builder.append("gem 'killbill'\n");
+ builder.append("require 'killbill'\n");
+ // Assume the plugin is shipped as a Gem
+ builder.append("begin\n")
+ .append("gem '").append(pluginGemName).append("'\n")
+ .append("rescue Gem::LoadError\n")
+ .append("warn \"WARN: unable to load gem ").append(pluginGemName).append("\"\n")
+ .append("end\n");
+ builder.append("begin\n")
+ .append("require '").append(pluginGemName).append("'\n")
+ .append("rescue LoadError\n")
+ // Could be useful for debugging
+ //.append("warn \"WARN: unable to require ").append(pluginGemName).append("\"\n")
+ .append("end\n");
+ // Load the extra require file, if specified
+ if (rubyRequire != null) {
+ builder.append("begin\n")
+ .append("require '").append(rubyRequire).append("'\n")
+ .append("rescue LoadError => e\n")
+ .append("warn \"WARN: unable to require ").append(rubyRequire).append(": \" + e.to_s\n")
+ .append("end\n");
+ }
+ // Require any file directly in the pluginLibdir directory (e.g. /var/tmp/bundles/ruby/foo/1.0/gems/*.rb).
+ // Although it is likely that any Killbill plugin will be distributed as a gem, it is still useful to
+ // be able to load individual scripts for prototyping/testing/...
+ builder.append("Dir.glob(ENV[\"GEM_HOME\"] + \"/*.rb\").each {|x| require x rescue warn \"WARN: unable to load #{x}\"}\n");
+ cachedRequireLine = builder.toString();
+ }
+ return cachedRequireLine;
+ }
+
+ private Ruby getRuntime() {
+ return pluginInstance.getMetaClass().getRuntime();
+ }
+
+ private ScriptingContainer setupScriptingContainer() {
+ // SINGLETHREAD model to avoid sharing state across scripting containers
+ // All calls are synchronized anyways (don't trust gems to be thread safe)
+ final ScriptingContainer scriptingContainer = new ScriptingContainer(LocalContextScope.SINGLETHREAD, LocalVariableBehavior.TRANSIENT, true);
+
+ // Set the load paths instead of adding, to avoid looking at the filesystem
+ scriptingContainer.setLoadPaths(Collections.<String>singletonList(pluginLibdir));
+
+ return scriptingContainer;
+ }
+
+ public enum VALIDATION_PLUGIN_TYPE {
+ NOTIFICATION,
+ PAYMENT,
+ CURRENCY,
+ NONE
+ }
+
+ protected abstract class PluginCallback {
+
+ private final VALIDATION_PLUGIN_TYPE pluginType;
+
+ public PluginCallback(final VALIDATION_PLUGIN_TYPE pluginType) {
+ this.pluginType = pluginType;
+ }
+
+ public abstract <T> T doCall(final Ruby runtime) throws PaymentPluginApiException;
+
+ public VALIDATION_PLUGIN_TYPE getPluginType() {
+ return pluginType;
+ }
+ }
+
+ protected <T> T callWithRuntimeAndChecking(final PluginCallback cb) throws PaymentPluginApiException {
+ synchronized (pluginMonitor) {
+ try {
+ checkPluginIsRunning();
+
+ switch (cb.getPluginType()) {
+ case NOTIFICATION:
+ checkValidNotificationPlugin();
+ break;
+ case PAYMENT:
+ checkValidPaymentPlugin();
+ break;
+ case CURRENCY:
+ checkValidCurrencyPlugin();
+ break;
+ default:
+ break;
+ }
+
+ final Ruby runtime = getRuntime();
+ return cb.doCall(runtime);
+ } catch (RuntimeException e) {
+ log.warn("RuntimeException in jruby plugin ", e);
+ throw e;
+ }
+ }
+ }
+}
osgi-bundles/bundles/logger/pom.xml 10(+5 -5)
diff --git a/osgi-bundles/bundles/logger/pom.xml b/osgi-bundles/bundles/logger/pom.xml
index 4362aef..d4bd15b 100644
--- a/osgi-bundles/bundles/logger/pom.xml
+++ b/osgi-bundles/bundles/logger/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-bundles-logger</artifactId>
@@ -55,9 +55,9 @@
</executions>
<configuration>
<instructions>
- <Bundle-Activator>com.ning.billing.osgi.bundles.logger.Activator</Bundle-Activator>
- <Export-Package></Export-Package>
- <Private-Package>com.ning.billing.osgi.bundles.logger.*</Private-Package>
+ <Bundle-Activator>org.killbill.billing.osgi.bundles.logger.Activator</Bundle-Activator>
+ <Export-Package />
+ <Private-Package>org.killbill.billing.osgi.bundles.logger.*</Private-Package>
<!-- Optional resolution because exported by the Felix system bundle -->
<Import-Package>*;resolution:=optional</Import-Package>
</instructions>
diff --git a/osgi-bundles/bundles/logger/src/main/java/org/killbill/billing/osgi/bundles/logger/Activator.java b/osgi-bundles/bundles/logger/src/main/java/org/killbill/billing/osgi/bundles/logger/Activator.java
new file mode 100644
index 0000000..80fed42
--- /dev/null
+++ b/osgi-bundles/bundles/logger/src/main/java/org/killbill/billing/osgi/bundles/logger/Activator.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.logger;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogListener;
+import org.osgi.service.log.LogReaderService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Activator implements BundleActivator {
+
+ private static final Logger logger = LoggerFactory.getLogger(Activator.class);
+
+ private final LogListener killbillLogListener = new KillbillLogWriter();
+ private final List<LogReaderService> logReaderServices = new LinkedList<LogReaderService>();
+
+ private final ServiceListener logReaderServiceListener = new ServiceListener() {
+ public void serviceChanged(final ServiceEvent event) {
+ final ServiceReference serviceReference = event.getServiceReference();
+ if (serviceReference == null || serviceReference.getBundle() == null) {
+ return;
+ }
+
+ final BundleContext bundleContext = serviceReference.getBundle().getBundleContext();
+ if (bundleContext == null) {
+ return;
+ }
+
+ final LogReaderService logReaderService = (LogReaderService) bundleContext.getService(serviceReference);
+ if (logReaderService != null) {
+ if (event.getType() == ServiceEvent.REGISTERED) {
+ registerLogReaderService(logReaderService);
+ } else if (event.getType() == ServiceEvent.UNREGISTERING) {
+ unregisterLogReaderService(logReaderService);
+ }
+ }
+ }
+ };
+
+ @Override
+ public void start(final BundleContext context) throws Exception {
+ final String filter = "(objectclass=" + LogReaderService.class.getName() + ")";
+ try {
+ context.addServiceListener(logReaderServiceListener, filter);
+ } catch (final InvalidSyntaxException e) {
+ logger.warn("Unable to register the killbill LogReaderService listener", e);
+ }
+
+ // If the LogReaderService was already registered, manually construct a REGISTERED ServiceEvent
+ final ServiceReference[] serviceReferences = context.getServiceReferences((String) null, filter);
+ for (int i = 0; serviceReferences != null && i < serviceReferences.length; i++) {
+ logReaderServiceListener.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, serviceReferences[i]));
+ }
+ }
+
+ @Override
+ public void stop(final BundleContext context) throws Exception {
+ for (final Iterator<LogReaderService> iterator = logReaderServices.iterator(); iterator.hasNext(); ) {
+ final LogReaderService service = iterator.next();
+ service.removeLogListener(killbillLogListener);
+ iterator.remove();
+ }
+ }
+
+ private void registerLogReaderService(final LogReaderService service) {
+ logger.info("Registering the killbill LogReaderService listener");
+ logReaderServices.add(service);
+ service.addLogListener(killbillLogListener);
+ }
+
+ private void unregisterLogReaderService(final LogReaderService logReaderService) {
+ logger.info("Unregistering the killbill LogReaderService listener");
+ logReaderService.removeLogListener(killbillLogListener);
+ logReaderServices.remove(logReaderService);
+ }
+}
diff --git a/osgi-bundles/bundles/logger/src/main/java/org/killbill/billing/osgi/bundles/logger/KillbillLogWriter.java b/osgi-bundles/bundles/logger/src/main/java/org/killbill/billing/osgi/bundles/logger/KillbillLogWriter.java
new file mode 100644
index 0000000..b71f7c1
--- /dev/null
+++ b/osgi-bundles/bundles/logger/src/main/java/org/killbill/billing/osgi/bundles/logger/KillbillLogWriter.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.logger;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.Version;
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogListener;
+import org.osgi.service.log.LogService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// Inspired by osgi-over-slf4j
+public class KillbillLogWriter implements LogListener {
+
+ private static final String UNKNOWN = "[Unknown]";
+
+ private final Map<String, Logger> delegates = new HashMap<String, Logger>();
+
+ // Invoked by the log service implementation for each log entry
+ public void logged(final LogEntry entry) {
+ final Bundle bundle = entry.getBundle();
+ final Logger delegate = getDelegateForBundle(bundle);
+
+ final ServiceReference serviceReference = entry.getServiceReference();
+ final int level = entry.getLevel();
+ final String message = entry.getMessage();
+ final Throwable exception = entry.getException();
+
+ if (serviceReference != null && exception != null) {
+ log(delegate, serviceReference, level, message, exception);
+ } else if (serviceReference != null) {
+ log(delegate, serviceReference, level, message);
+ } else if (exception != null) {
+ log(delegate, level, message, exception);
+ } else {
+ log(delegate, level, message);
+ }
+ }
+
+ private Logger getDelegateForBundle(/* @Nullable */ final Bundle bundle) {
+ final String loggerName;
+ if (bundle != null) {
+ final String name = bundle.getSymbolicName();
+ Version version = bundle.getVersion();
+ if (version == null) {
+ version = Version.emptyVersion;
+ }
+ loggerName = name + '.' + version;
+ } else {
+ loggerName = KillbillLogWriter.class.getName();
+ }
+
+ if (delegates.get(loggerName) == null) {
+ synchronized (delegates) {
+ if (delegates.get(loggerName) == null) {
+ delegates.put(loggerName, LoggerFactory.getLogger(loggerName));
+ }
+ }
+ }
+
+ return delegates.get(loggerName);
+ }
+
+ private void log(final Logger delegate, final int level, final String message) {
+ switch (level) {
+ case LogService.LOG_DEBUG:
+ delegate.debug(message);
+ break;
+ case LogService.LOG_ERROR:
+ delegate.error(message);
+ break;
+ case LogService.LOG_INFO:
+ delegate.info(message);
+ break;
+ case LogService.LOG_WARNING:
+ delegate.warn(message);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void log(final Logger delegate, final int level, final String message, final Throwable exception) {
+ switch (level) {
+ case LogService.LOG_DEBUG:
+ delegate.debug(message, exception);
+ break;
+ case LogService.LOG_ERROR:
+ delegate.error(message, exception);
+ break;
+ case LogService.LOG_INFO:
+ delegate.info(message, exception);
+ break;
+ case LogService.LOG_WARNING:
+ delegate.warn(message, exception);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void log(final Logger delegate, final ServiceReference sr, final int level, final String message) {
+ switch (level) {
+ case LogService.LOG_DEBUG:
+ if (delegate.isDebugEnabled()) {
+ delegate.debug(createMessage(sr, message));
+ }
+ break;
+ case LogService.LOG_ERROR:
+ if (delegate.isErrorEnabled()) {
+ delegate.error(createMessage(sr, message));
+ }
+ break;
+ case LogService.LOG_INFO:
+ if (delegate.isInfoEnabled()) {
+ delegate.info(createMessage(sr, message));
+ }
+ break;
+ case LogService.LOG_WARNING:
+ if (delegate.isWarnEnabled()) {
+ delegate.warn(createMessage(sr, message));
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void log(final Logger delegate, final ServiceReference sr, final int level, final String message, final Throwable exception) {
+ switch (level) {
+ case LogService.LOG_DEBUG:
+ if (delegate.isDebugEnabled()) {
+ delegate.debug(createMessage(sr, message), exception);
+ }
+ break;
+ case LogService.LOG_ERROR:
+ if (delegate.isErrorEnabled()) {
+ delegate.error(createMessage(sr, message), exception);
+ }
+ break;
+ case LogService.LOG_INFO:
+ if (delegate.isInfoEnabled()) {
+ delegate.info(createMessage(sr, message), exception);
+ }
+ break;
+ case LogService.LOG_WARNING:
+ if (delegate.isWarnEnabled()) {
+ delegate.warn(createMessage(sr, message), exception);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Formats the log message to indicate the service sending it, if known.
+ *
+ * @param sr the ServiceReference sending the message.
+ * @param message The message to log.
+ * @return The formatted log message.
+ */
+ private String createMessage(final ServiceReference sr, final String message) {
+ final StringBuilder output = new StringBuilder();
+ if (sr != null) {
+ output.append('[').append(sr.toString()).append(']');
+ } else {
+ output.append(UNKNOWN);
+ }
+ output.append(message);
+
+ return output.toString();
+ }
+}
osgi-bundles/bundles/meter/pom.xml 31(+8 -23)
diff --git a/osgi-bundles/bundles/meter/pom.xml b/osgi-bundles/bundles/meter/pom.xml
index 0bc7acc..9e5a68b 100644
--- a/osgi-bundles/bundles/meter/pom.xml
+++ b/osgi-bundles/bundles/meter/pom.xml
@@ -18,7 +18,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<version>0.3.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -57,34 +57,29 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.jolbox</groupId>
<artifactId>bonecp</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
@@ -103,16 +98,6 @@
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>org.antlr</groupId>
<artifactId>stringtemplate</artifactId>
<scope>runtime</scope>
@@ -165,8 +150,8 @@
</executions>
<configuration>
<instructions>
- <Bundle-Activator>com.ning.billing.meter.osgi.MeterActivator</Bundle-Activator>
- <Import-Package>*;resolution:=optional,com.ning.billing.osgi.api</Import-Package>
+ <Bundle-Activator>org.killbill.billing.meter.osgi.MeterActivator</Bundle-Activator>
+ <Import-Package>*;resolution:=optional,org.killbill.billing.osgi.api</Import-Package>
</instructions>
</configuration>
</plugin>
diff --git a/osgi-bundles/bundles/meter/src/main/java/org/killbill/billing/meter/jaxrs/resources/MeterResource.java b/osgi-bundles/bundles/meter/src/main/java/org/killbill/billing/meter/jaxrs/resources/MeterResource.java
new file mode 100644
index 0000000..68977e9
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/main/java/org/killbill/billing/meter/jaxrs/resources/MeterResource.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.jaxrs.resources;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.StreamingOutput;
+
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+
+import org.killbill.billing.meter.api.TimeAggregationMode;
+import org.killbill.billing.meter.api.user.MeterUserApi;
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.CallContextFactory;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.clock.Clock;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+@Path(MeterResource.METER_PATH)
+public class MeterResource {
+
+ public static final String METER_PATH = "/1.0/kb/plugins/meter";
+
+ private static final String HDR_CREATED_BY = "X-Killbill-CreatedBy";
+ private static final String HDR_REASON = "X-Killbill-Reason";
+ private static final String HDR_COMMENT = "X-Killbill-Comment";
+ private static final String STRING_PATTERN = "[\\w-]+";
+ private static final String QUERY_METER_WITH_CATEGORY_AGGREGATE = "withCategoryAggregate";
+ private static final String QUERY_METER_TIME_AGGREGATION_MODE = "timeAggregationMode";
+ private static final String QUERY_METER_TIMESTAMP = "timestamp";
+ private static final String QUERY_METER_FROM = "from";
+ private static final String QUERY_METER_TO = "to";
+ private static final String QUERY_METER_CATEGORY = "category";
+ private static final String QUERY_METER_CATEGORY_AND_METRIC = "category_and_metric";
+
+ private final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTimeParser();
+
+ private final MeterUserApi meterApi;
+ private final Clock clock;
+ private final CallContextFactory contextFactory;
+
+ @Inject
+ public MeterResource(final MeterUserApi meterApi,
+ final Clock clock,
+ final CallContextFactory factory) {
+ this.meterApi = meterApi;
+ this.clock = clock;
+ this.contextFactory = factory;
+ }
+
+ @GET
+ @Path("/{source:" + STRING_PATTERN + "}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public StreamingOutput getUsage(@PathParam("source") final String source,
+ // Aggregates per category
+ @QueryParam(QUERY_METER_CATEGORY) final List<String> categories,
+ // Format: category,metric
+ @QueryParam(QUERY_METER_CATEGORY_AND_METRIC) final List<String> categoriesAndMetrics,
+ @QueryParam(QUERY_METER_FROM) final String fromTimestampString,
+ @QueryParam(QUERY_METER_TO) final String toTimestampString,
+ @QueryParam(QUERY_METER_TIME_AGGREGATION_MODE) @DefaultValue("") final String timeAggregationModeString,
+ @javax.ws.rs.core.Context final HttpServletRequest request) {
+ final TenantContext tenantContext = createContext(request);
+
+ final DateTime fromTimestamp;
+ if (fromTimestampString != null) {
+ fromTimestamp = DATE_TIME_FORMATTER.parseDateTime(fromTimestampString);
+ } else {
+ fromTimestamp = clock.getUTCNow().minusMonths(3);
+ }
+ final DateTime toTimestamp;
+ if (toTimestampString != null) {
+ toTimestamp = DATE_TIME_FORMATTER.parseDateTime(toTimestampString);
+ } else {
+ toTimestamp = clock.getUTCNow();
+ }
+
+ return new StreamingOutput() {
+ @Override
+ public void write(final OutputStream output) throws IOException, WebApplicationException {
+ // Look at aggregates per category?
+ if (categories != null && categories.size() > 0) {
+ if (Strings.isNullOrEmpty(timeAggregationModeString)) {
+ meterApi.getUsage(output, source, categories, fromTimestamp, toTimestamp, tenantContext);
+ } else {
+ final TimeAggregationMode timeAggregationMode = TimeAggregationMode.valueOf(timeAggregationModeString);
+ meterApi.getUsage(output, timeAggregationMode, source, categories, fromTimestamp, toTimestamp, tenantContext);
+ }
+ } else {
+ final Map<String, Collection<String>> metricsPerCategory = retrieveMetricsPerCategory(categoriesAndMetrics);
+ if (Strings.isNullOrEmpty(timeAggregationModeString)) {
+ meterApi.getUsage(output, source, metricsPerCategory, fromTimestamp, toTimestamp, tenantContext);
+ } else {
+ final TimeAggregationMode timeAggregationMode = TimeAggregationMode.valueOf(timeAggregationModeString);
+ meterApi.getUsage(output, timeAggregationMode, source, metricsPerCategory, fromTimestamp, toTimestamp, tenantContext);
+ }
+ }
+ }
+ };
+ }
+
+ private Map<String, Collection<String>> retrieveMetricsPerCategory(final List<String> categoriesAndMetrics) {
+ final Map<String, Collection<String>> metricsPerCategory = new HashMap<String, Collection<String>>();
+ for (final String categoryAndSampleKind : categoriesAndMetrics) {
+ final String[] categoryAndMetric = getCategoryAndMetricFromQueryParameter(categoryAndSampleKind);
+ if (metricsPerCategory.get(categoryAndMetric[0]) == null) {
+ metricsPerCategory.put(categoryAndMetric[0], new ArrayList<String>());
+ }
+
+ metricsPerCategory.get(categoryAndMetric[0]).add(categoryAndMetric[1]);
+ }
+
+ return metricsPerCategory;
+ }
+
+ private String[] getCategoryAndMetricFromQueryParameter(final String categoryAndMetric) {
+ final String[] parts = categoryAndMetric.split(",");
+ if (parts.length != 2) {
+ throw new WebApplicationException(Response.Status.BAD_REQUEST);
+ }
+ return parts;
+ }
+
+ @POST
+ @Path("/{source:" + STRING_PATTERN + "}/{categoryName:" + STRING_PATTERN + "}/{metricName:" + STRING_PATTERN + "}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response recordUsage(@PathParam("source") final String source,
+ @PathParam("categoryName") final String categoryName,
+ @PathParam("metricName") final String metricName,
+ @QueryParam(QUERY_METER_WITH_CATEGORY_AGGREGATE) @DefaultValue("false") final Boolean withAggregate,
+ @QueryParam(QUERY_METER_TIMESTAMP) final String timestampString,
+ @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) {
+ final CallContext callContext = createContext(createdBy, reason, comment, request);
+
+ final DateTime timestamp;
+ if (timestampString == null) {
+ timestamp = clock.getUTCNow();
+ } else {
+ timestamp = DATE_TIME_FORMATTER.parseDateTime(timestampString);
+ }
+
+ if (withAggregate) {
+ meterApi.incrementUsageAndAggregate(source, categoryName, metricName, timestamp, callContext);
+ } else {
+ meterApi.incrementUsage(source, categoryName, metricName, timestamp, callContext);
+ }
+
+ return Response.ok().build();
+ }
+
+ private CallContext createContext(final String createdBy, final String reason, final String comment, final ServletRequest request)
+ throws IllegalArgumentException {
+ try {
+ Preconditions.checkNotNull(createdBy, String.format("Header %s needs to be set", HDR_CREATED_BY));
+ final Tenant tenant = getTenantFromRequest(request);
+ return contextFactory.createCallContext(tenant == null ? null : tenant.getId(), createdBy, CallOrigin.EXTERNAL, UserType.CUSTOMER, reason,
+ comment, UUID.randomUUID());
+ } catch (NullPointerException e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+
+ private TenantContext createContext(final ServletRequest request) {
+ final Tenant tenant = getTenantFromRequest(request);
+ if (tenant == null) {
+ // Multi-tenancy may not have been configured - default to "default" tenant (see InternalCallContextFactory)
+ return contextFactory.createTenantContext(null);
+ } else {
+ return contextFactory.createTenantContext(tenant.getId());
+ }
+ }
+
+ private Tenant getTenantFromRequest(final ServletRequest request) {
+ final Object tenantObject = request.getAttribute("killbill_tenant");
+ if (tenantObject == null) {
+ return null;
+ } else {
+ return (Tenant) tenantObject;
+ }
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/MeterTestSuite.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/MeterTestSuite.java
new file mode 100644
index 0000000..bf70702
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/MeterTestSuite.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter;
+
+import java.lang.reflect.Method;
+import java.util.UUID;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.ITestResult;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
+
+public class MeterTestSuite {
+
+ // Use the simple name here to save screen real estate
+ protected static final Logger log = LoggerFactory.getLogger(MeterTestSuiteNoDB.class.getSimpleName());
+
+ private boolean hasFailed = false;
+
+ protected Clock clock = new ClockMock();
+
+ protected final InternalCallContext internalCallContext = new InternalCallContext(InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID, 1687L, UUID.randomUUID(),
+ UUID.randomUUID().toString(), CallOrigin.TEST,
+ UserType.TEST, "Testing", "This is a test",
+ clock.getUTCNow(), clock.getUTCNow());
+
+ @BeforeMethod(alwaysRun = true)
+ public void startTestSuite(final Method method) throws Exception {
+ log.info("***************************************************************************************************");
+ log.info("*** Starting test {}:{}", method.getDeclaringClass().getName(), method.getName());
+ log.info("***************************************************************************************************");
+ }
+
+ @AfterMethod(alwaysRun = true)
+ public void endTestSuite(final Method method, final ITestResult result) throws Exception {
+ log.info("***************************************************************************************************");
+ log.info("*** Ending test {}:{} {} ({} s.)", method.getDeclaringClass().getName(), method.getName(),
+ result.isSuccess() ? "SUCCESS" : "!!! FAILURE !!!",
+ (result.getEndMillis() - result.getStartMillis()) / 1000);
+ log.info("***************************************************************************************************");
+ if (!hasFailed && !result.isSuccess()) {
+ hasFailed = true;
+ }
+ }
+
+ public boolean hasFailed() {
+ return hasFailed;
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/aggregator/TestTimelineAggregator.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/aggregator/TestTimelineAggregator.java
new file mode 100644
index 0000000..97935a6
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/aggregator/TestTimelineAggregator.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.timeline.aggregator;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.meter.MeterTestSuiteWithEmbeddedDB;
+import org.killbill.billing.meter.timeline.TimelineSourceEventAccumulator;
+import org.killbill.billing.meter.timeline.chunks.TimelineChunk;
+import org.killbill.billing.meter.timeline.codec.DefaultSampleCoder;
+import org.killbill.billing.meter.timeline.codec.SampleCoder;
+import org.killbill.billing.meter.timeline.consumer.TimelineChunkConsumer;
+import org.killbill.billing.meter.timeline.persistent.DefaultTimelineDao;
+import org.killbill.billing.meter.timeline.persistent.TimelineDao;
+import org.killbill.billing.meter.timeline.samples.SampleOpcode;
+import org.killbill.billing.meter.timeline.samples.ScalarSample;
+import org.killbill.billing.meter.timeline.sources.SourceSamplesForTimestamp;
+import org.killbill.billing.meter.timeline.times.DefaultTimelineCoder;
+import org.killbill.billing.meter.timeline.times.TimelineCoder;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.util.config.MeterConfig;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class TestTimelineAggregator extends MeterTestSuiteWithEmbeddedDB {
+
+ private static final UUID HOST_UUID = UUID.randomUUID();
+ private static final String HOST_NAME = HOST_UUID.toString();
+ private static final String EVENT_TYPE = "myType";
+ private static final int EVENT_TYPE_ID = 123;
+ private static final String MIN_HEAPUSED_KIND = "min_heapUsed";
+ private static final String MAX_HEAPUSED_KIND = "max_heapUsed";
+ private static final DateTime START_TIME = new DateTime(DateTimeZone.UTC);
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+ private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+ private final NonEntityDao nonEntityDao = Mockito.mock(NonEntityDao.class);
+ private final InternalCallContextFactory internalCallContextFactory = new InternalCallContextFactory(new ClockMock(), nonEntityDao, new CacheControllerDispatcher());
+
+ private TimelineDao timelineDao;
+ private TimelineAggregator aggregator;
+
+ private Integer hostId = null;
+ private Integer minHeapUsedKindId = null;
+ private Integer maxHeapUsedKindId = null;
+
+ @BeforeMethod(groups = "mysql")
+ public void setUp() throws Exception {
+ timelineDao = new DefaultTimelineDao(getDBI());
+ final Properties properties = System.getProperties();
+ properties.put("killbill.usage.timelines.chunksToAggregate", "2,2");
+ final MeterConfig config = new ConfigurationObjectFactory(properties).build(MeterConfig.class);
+ aggregator = new TimelineAggregator(getDBI(), timelineDao, timelineCoder, sampleCoder, config, internalCallContextFactory);
+ }
+
+ @Test(groups = "mysql")
+ public void testAggregation() throws Exception {
+ // Create the host
+ hostId = timelineDao.getOrAddSource(HOST_NAME, internalCallContext);
+ Assert.assertNotNull(hostId);
+ Assert.assertEquals(timelineDao.getSources(internalCallContext).values().size(), 1);
+
+ // Create the sample kinds
+ minHeapUsedKindId = timelineDao.getOrAddMetric(EVENT_TYPE_ID, MIN_HEAPUSED_KIND, internalCallContext);
+ Assert.assertNotNull(minHeapUsedKindId);
+ maxHeapUsedKindId = timelineDao.getOrAddMetric(EVENT_TYPE_ID, MAX_HEAPUSED_KIND, internalCallContext);
+ Assert.assertNotNull(maxHeapUsedKindId);
+ Assert.assertEquals(timelineDao.getMetrics(internalCallContext).values().size(), 2);
+
+ // Create two sets of times: T - 125 ... T - 65 ; T - 60 ... T (note the gap!)
+ createAOneHourTimelineChunk(125);
+ createAOneHourTimelineChunk(60);
+
+ // Check the getSamplesByHostIdsAndSampleKindIds DAO method works as expected
+ // You might want to draw timelines on a paper and remember boundaries are inclusive to understand these numbers
+ checkSamplesForATimeline(185, 126, 0);
+ checkSamplesForATimeline(185, 125, 2);
+ checkSamplesForATimeline(64, 61, 0);
+ checkSamplesForATimeline(125, 65, 2);
+ checkSamplesForATimeline(60, 0, 2);
+ checkSamplesForATimeline(125, 0, 4);
+ checkSamplesForATimeline(124, 0, 4);
+ checkSamplesForATimeline(124, 66, 2);
+
+ aggregator.getAndProcessTimelineAggregationCandidates();
+
+ Assert.assertEquals(timelineDao.getSources(internalCallContext).values().size(), 1);
+ Assert.assertEquals(timelineDao.getMetrics(internalCallContext).values().size(), 2);
+
+ // Similar than above, but we have only 2 now
+ checkSamplesForATimeline(185, 126, 0);
+ checkSamplesForATimeline(185, 125, 2);
+ // Note, the gap is filled now
+ checkSamplesForATimeline(64, 61, 2);
+ checkSamplesForATimeline(125, 65, 2);
+ checkSamplesForATimeline(60, 0, 2);
+ checkSamplesForATimeline(125, 0, 2);
+ checkSamplesForATimeline(124, 0, 2);
+ checkSamplesForATimeline(124, 66, 2);
+ }
+
+ private void checkSamplesForATimeline(final Integer startTimeMinutesAgo, final Integer endTimeMinutesAgo, final long expectedChunks) throws InterruptedException {
+ final AtomicLong timelineChunkSeen = new AtomicLong(0);
+
+ timelineDao.getSamplesBySourceIdsAndMetricIds(ImmutableList.<Integer>of(hostId), ImmutableList.<Integer>of(minHeapUsedKindId, maxHeapUsedKindId),
+ START_TIME.minusMinutes(startTimeMinutesAgo), START_TIME.minusMinutes(endTimeMinutesAgo), new TimelineChunkConsumer() {
+
+ @Override
+ public void processTimelineChunk(final TimelineChunk chunk) {
+ Assert.assertEquals((Integer) chunk.getSourceId(), hostId);
+ Assert.assertTrue(chunk.getMetricId() == minHeapUsedKindId || chunk.getMetricId() == maxHeapUsedKindId);
+ timelineChunkSeen.incrementAndGet();
+ }
+ }, internalCallContext);
+
+ Assert.assertEquals(timelineChunkSeen.get(), expectedChunks);
+ }
+
+ private void createAOneHourTimelineChunk(final int startTimeMinutesAgo) throws IOException {
+ final DateTime firstSampleTime = START_TIME.minusMinutes(startTimeMinutesAgo);
+ final TimelineSourceEventAccumulator accumulator = new TimelineSourceEventAccumulator(timelineDao, timelineCoder, sampleCoder, hostId, EVENT_TYPE_ID, firstSampleTime, internalCallContextFactory);
+ // 120 samples per hour
+ for (int i = 0; i < 120; i++) {
+ final DateTime eventDateTime = firstSampleTime.plusSeconds(i * 30);
+ final Map<Integer, ScalarSample> event = createEvent(eventDateTime.getMillis());
+ final SourceSamplesForTimestamp samples = new SourceSamplesForTimestamp(hostId, EVENT_TYPE, eventDateTime, event);
+ accumulator.addSourceSamples(samples);
+ }
+
+ accumulator.extractAndQueueTimelineChunks();
+ }
+
+ private Map<Integer, ScalarSample> createEvent(final long ts) {
+ return ImmutableMap.<Integer, ScalarSample>of(
+ minHeapUsedKindId, new ScalarSample(SampleOpcode.LONG, Long.MIN_VALUE + ts),
+ maxHeapUsedKindId, new ScalarSample(SampleOpcode.LONG, Long.MAX_VALUE - ts)
+ );
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/chunks/TestTimelineChunk.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/chunks/TestTimelineChunk.java
new file mode 100644
index 0000000..ae5b071
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/chunks/TestTimelineChunk.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.timeline.chunks;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.meter.MeterTestSuiteNoDB;
+import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
+
+public class TestTimelineChunk extends MeterTestSuiteNoDB {
+
+ private final Clock clock = new ClockMock();
+
+ @Test(groups = "fast")
+ public void testGetters() throws Exception {
+ final long chunkId = 0L;
+ final int sourceId = 1;
+ final int metricId = 2;
+ final DateTime startTime = clock.getUTCNow();
+ final DateTime endTime = startTime.plusDays(2);
+ final byte[] timeBytes = new byte[]{0x1, 0x2, 0x3};
+ final byte[] sampleBytes = new byte[]{0xA, 0xB, 0xC};
+ final TimelineChunk timelineChunk = new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+
+ Assert.assertEquals(timelineChunk.getChunkId(), chunkId);
+ Assert.assertEquals(timelineChunk.getSourceId(), sourceId);
+ Assert.assertEquals(timelineChunk.getMetricId(), metricId);
+ Assert.assertEquals(timelineChunk.getStartTime(), startTime);
+ Assert.assertEquals(timelineChunk.getEndTime(), endTime);
+ Assert.assertEquals(timelineChunk.getTimeBytesAndSampleBytes().getTimeBytes(), timeBytes);
+ Assert.assertEquals(timelineChunk.getTimeBytesAndSampleBytes().getSampleBytes(), sampleBytes);
+ Assert.assertEquals(timelineChunk.getAggregationLevel(), 0);
+ Assert.assertFalse(timelineChunk.getNotValid());
+ Assert.assertFalse(timelineChunk.getDontAggregate());
+ }
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final long chunkId = 0L;
+ final int sourceId = 1;
+ final int metricId = 2;
+ final DateTime startTime = clock.getUTCNow();
+ final DateTime endTime = startTime.plusDays(2);
+ final byte[] timeBytes = new byte[]{0x1, 0x2, 0x3};
+ final byte[] sampleBytes = new byte[]{0xA, 0xB, 0xC};
+
+ final TimelineChunk timelineChunk = new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+ Assert.assertEquals(timelineChunk, timelineChunk);
+
+ final TimelineChunk sameTimelineChunk = new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+ Assert.assertEquals(sameTimelineChunk, timelineChunk);
+
+ final TimelineChunk otherTimelineChunk = new TimelineChunk(sourceId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+ Assert.assertNotEquals(otherTimelineChunk, timelineChunk);
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/consumer/TestAccumulatorSampleConsumer.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/consumer/TestAccumulatorSampleConsumer.java
new file mode 100644
index 0000000..0881613
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/consumer/TestAccumulatorSampleConsumer.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.timeline.consumer;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.meter.MeterTestSuiteNoDB;
+import org.killbill.billing.meter.api.TimeAggregationMode;
+import org.killbill.billing.meter.timeline.samples.SampleOpcode;
+import org.killbill.clock.ClockMock;
+
+public class TestAccumulatorSampleConsumer extends MeterTestSuiteNoDB {
+
+ private final ClockMock clock = new ClockMock();
+
+ @Test(groups = "fast")
+ public void testDailyAggregation() throws Exception {
+ clock.setTime(new DateTime(2012, 12, 1, 12, 40, DateTimeZone.UTC));
+ final DateTime start = clock.getUTCNow();
+
+ final AccumulatorSampleConsumer sampleConsumer = new AccumulatorSampleConsumer(TimeAggregationMode.DAYS, new CSVSampleProcessor());
+
+ // 5 for day 1
+ sampleConsumer.processOneSample(start, SampleOpcode.DOUBLE, (double) 1);
+ sampleConsumer.processOneSample(start.plusHours(4), SampleOpcode.DOUBLE, (double) 4);
+ // 1 for day 2
+ sampleConsumer.processOneSample(start.plusDays(1), SampleOpcode.DOUBLE, (double) 1);
+ // 10 and 20 for day 3 (with different opcode)
+ sampleConsumer.processOneSample(start.plusDays(2), SampleOpcode.DOUBLE, (double) 10);
+ sampleConsumer.processOneSample(start.plusDays(2), SampleOpcode.INT, 20);
+
+ Assert.assertEquals(sampleConsumer.flush(), "1354320000,5.0,1354406400,1.0,1354492800,10.0,1354492800,20.0");
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/persistent/TestFileBackedBuffer.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/persistent/TestFileBackedBuffer.java
new file mode 100644
index 0000000..62a971b
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/persistent/TestFileBackedBuffer.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.timeline.persistent;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.skife.config.ConfigurationObjectFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.meter.MeterTestSuiteNoDB;
+import org.killbill.billing.meter.timeline.BackgroundDBChunkWriter;
+import org.killbill.billing.meter.timeline.MockTimelineDao;
+import org.killbill.billing.meter.timeline.TimelineEventHandler;
+import org.killbill.billing.meter.timeline.codec.DefaultSampleCoder;
+import org.killbill.billing.meter.timeline.codec.SampleCoder;
+import org.killbill.billing.meter.timeline.sources.SourceSamplesForTimestamp;
+import org.killbill.billing.meter.timeline.times.DefaultTimelineCoder;
+import org.killbill.billing.meter.timeline.times.TimelineCoder;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.util.config.MeterConfig;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import com.google.common.collect.ImmutableMap;
+
+public class TestFileBackedBuffer extends MeterTestSuiteNoDB {
+
+ private static final Logger log = LoggerFactory.getLogger(TestFileBackedBuffer.class);
+
+ private static final UUID HOST_UUID = UUID.randomUUID();
+ private static final String KIND_A = "kindA";
+ private static final String KIND_B = "kindB";
+ private static final Map<String, Object> EVENT = ImmutableMap.<String, Object>of(KIND_A, 12, KIND_B, 42);
+ // ~105 bytes per event, 10 1MB buffers -> need at least 100,000 events to spill over
+ private static final int NB_EVENTS = 100000;
+ private static final File basePath = new File(System.getProperty("java.io.tmpdir"), "TestFileBackedBuffer-" + System.currentTimeMillis());
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+ private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+ private final NonEntityDao nonEntityDao = Mockito.mock(NonEntityDao.class);
+ private final InternalCallContextFactory internalCallContextFactory = new InternalCallContextFactory(new ClockMock(), nonEntityDao, new CacheControllerDispatcher());
+ private final TimelineDao dao = new MockTimelineDao();
+ private TimelineEventHandler timelineEventHandler;
+
+ @BeforeMethod(groups = "fast")
+ public void setUp() throws Exception {
+ Assert.assertTrue(basePath.mkdir());
+ System.setProperty("killbill.usage.timelines.spoolDir", basePath.getAbsolutePath());
+ System.setProperty("killbill.usage.timelines.length", "60s");
+ final MeterConfig config = new ConfigurationObjectFactory(System.getProperties()).build(MeterConfig.class);
+ timelineEventHandler = new TimelineEventHandler(config, dao, timelineCoder, sampleCoder, new BackgroundDBChunkWriter(dao, config, internalCallContextFactory),
+ new FileBackedBuffer(config.getSpoolDir(), "TimelineEventHandler", 1024 * 1024, 10));
+
+ dao.getOrAddSource(HOST_UUID.toString(), internalCallContext);
+ }
+
+ @Test(groups = "fast") // Not really fast, but doesn't require a database
+ public void testAppend() throws Exception {
+ log.info("Writing files to " + basePath);
+ final List<File> binFiles = new ArrayList<File>();
+
+ final List<DateTime> timestampsRecorded = new ArrayList<DateTime>();
+ final List<String> categoriesRecorded = new ArrayList<String>();
+
+ // Sanity check before the tests
+ Assert.assertEquals(timelineEventHandler.getBackingBuffer().getFilesCreated(), 0);
+ findBinFiles(binFiles, basePath);
+ Assert.assertEquals(binFiles.size(), 0);
+
+ // Send enough events to spill over to disk
+ final DateTime startTime = new DateTime(DateTimeZone.UTC);
+ for (int i = 0; i < NB_EVENTS; i++) {
+ final String category = UUID.randomUUID().toString();
+ final DateTime eventTimestamp = startTime.plusSeconds(i);
+ timelineEventHandler.record(HOST_UUID.toString(), category, eventTimestamp, EVENT, internalCallContext);
+ timestampsRecorded.add(eventTimestamp);
+ categoriesRecorded.add(category);
+ }
+
+ // Check the files have been created (at least one per accumulator)
+ final long bytesOnDisk = timelineEventHandler.getBackingBuffer().getBytesOnDisk();
+ Assert.assertTrue(timelineEventHandler.getBackingBuffer().getFilesCreated() > 0);
+ binFiles.clear();
+ findBinFiles(binFiles, basePath);
+ Assert.assertTrue(binFiles.size() > 0);
+
+ log.info("Sent {} events and wrote {} bytes on disk ({} bytes/event)", new Object[]{NB_EVENTS, bytesOnDisk, bytesOnDisk / NB_EVENTS});
+
+ // Replay the events. Note that size of timestamp recorded != eventsReplayed as some of the ones sent are still in memory
+ final Replayer replayer = new Replayer(basePath.getAbsolutePath());
+ final List<SourceSamplesForTimestamp> eventsReplayed = replayer.readAll();
+ for (int i = 0; i < eventsReplayed.size(); i++) {
+ // Looks like Jackson maps it back using the JVM timezone
+ Assert.assertEquals(eventsReplayed.get(i).getTimestamp().toDateTime(DateTimeZone.UTC), timestampsRecorded.get(i));
+ Assert.assertEquals(eventsReplayed.get(i).getCategory(), categoriesRecorded.get(i));
+ }
+
+ // Make sure files have been deleted
+ binFiles.clear();
+ findBinFiles(binFiles, basePath);
+ Assert.assertEquals(binFiles.size(), 0);
+ }
+
+ private static void findBinFiles(final Collection<File> files, final File directory) {
+ final File[] found = directory.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(final File pathname) {
+ return pathname.getName().endsWith(".bin");
+ }
+ });
+ if (found != null) {
+ for (final File file : found) {
+ if (file.isDirectory()) {
+ findBinFiles(files, file);
+ } else {
+ files.add(file);
+ }
+ }
+ }
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/persistent/TestSamplesReplayer.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/persistent/TestSamplesReplayer.java
new file mode 100644
index 0000000..1c84161
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/persistent/TestSamplesReplayer.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.timeline.persistent;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.meter.MeterTestSuiteNoDB;
+import org.killbill.billing.meter.timeline.MockTimelineDao;
+import org.killbill.billing.meter.timeline.TimelineSourceEventAccumulator;
+import org.killbill.billing.meter.timeline.chunks.TimelineChunk;
+import org.killbill.billing.meter.timeline.codec.DefaultSampleCoder;
+import org.killbill.billing.meter.timeline.codec.SampleCoder;
+import org.killbill.billing.meter.timeline.samples.SampleOpcode;
+import org.killbill.billing.meter.timeline.samples.ScalarSample;
+import org.killbill.billing.meter.timeline.sources.SourceSamplesForTimestamp;
+import org.killbill.billing.meter.timeline.times.DefaultTimelineCoder;
+import org.killbill.billing.meter.timeline.times.TimelineCoder;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import com.google.common.collect.ImmutableMap;
+
+// Lightweight version of TestFileBackedBuffer
+public class TestSamplesReplayer extends MeterTestSuiteNoDB {
+
+ // Total space: 255 * 3 = 765 bytes
+ private static final int NB_EVENTS = 3;
+ // One will still be in memory after the flush
+ private static final int EVENTS_ON_DISK = NB_EVENTS - 1;
+ private static final int HOST_ID = 1;
+ private static final int EVENT_CATEGORY_ID = 123;
+ private static final File basePath = new File(System.getProperty("java.io.tmpdir"), "TestSamplesReplayer-" + System.currentTimeMillis());
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+ private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+ private final NonEntityDao nonEntityDao = Mockito.mock(NonEntityDao.class);
+ private final InternalCallContextFactory internalCallContextFactory = new InternalCallContextFactory(new ClockMock(), nonEntityDao, new CacheControllerDispatcher());
+
+ @BeforeMethod(groups = "fast")
+ public void setUp() throws Exception {
+ Assert.assertTrue(basePath.mkdir());
+ }
+
+ @Test(groups = "fast")
+ public void testIdentityFilter() throws Exception {
+ // Need less than 765 + 1 (metadata) bytes
+ final FileBackedBuffer fileBackedBuffer = new FileBackedBuffer(basePath.toString(), "test", 765, 1);
+
+ // Create the host samples - this will take 255 bytes
+ final Map<Integer, ScalarSample> eventMap = new HashMap<Integer, ScalarSample>();
+ eventMap.putAll(ImmutableMap.<Integer, ScalarSample>of(
+ 1, new ScalarSample(SampleOpcode.BYTE, (byte) 0),
+ 2, new ScalarSample(SampleOpcode.SHORT, (short) 1),
+ 3, new ScalarSample(SampleOpcode.INT, 1000),
+ 4, new ScalarSample(SampleOpcode.LONG, 12345678901L),
+ 5, new ScalarSample(SampleOpcode.DOUBLE, Double.MAX_VALUE)
+ ));
+ eventMap.putAll(ImmutableMap.<Integer, ScalarSample>of(
+ 6, new ScalarSample(SampleOpcode.FLOAT, Float.NEGATIVE_INFINITY),
+ 7, new ScalarSample(SampleOpcode.STRING, "pwet")
+ ));
+ final DateTime firstTime = new DateTime(DateTimeZone.UTC).minusSeconds(NB_EVENTS * 30);
+
+ // Write the samples to disk
+ for (int i = 0; i < NB_EVENTS; i++) {
+ final SourceSamplesForTimestamp samples = new SourceSamplesForTimestamp(HOST_ID, "something", firstTime.plusSeconds(30 * i), eventMap);
+ fileBackedBuffer.append(samples);
+ }
+
+ // Try the replayer
+ final Replayer replayer = new Replayer(new File(basePath.toString()).getAbsolutePath());
+ final List<SourceSamplesForTimestamp> hostSamples = replayer.readAll();
+ Assert.assertEquals(hostSamples.size(), EVENTS_ON_DISK);
+
+ // Try to encode them again
+ final MockTimelineDao dao = new MockTimelineDao();
+ final TimelineSourceEventAccumulator accumulator = new TimelineSourceEventAccumulator(dao, timelineCoder, sampleCoder, HOST_ID,
+ EVENT_CATEGORY_ID, hostSamples.get(0).getTimestamp(), internalCallContextFactory);
+ for (final SourceSamplesForTimestamp samplesFound : hostSamples) {
+ accumulator.addSourceSamples(samplesFound);
+ }
+ Assert.assertTrue(accumulator.checkSampleCounts(EVENTS_ON_DISK));
+
+ // This will check the SampleCode can encode value correctly
+ accumulator.extractAndQueueTimelineChunks();
+ Assert.assertEquals(dao.getTimelineChunks().keySet().size(), 7);
+ for (final TimelineChunk chunk : dao.getTimelineChunks().values()) {
+ Assert.assertEquals(chunk.getSourceId(), HOST_ID);
+ Assert.assertEquals(chunk.getSampleCount(), EVENTS_ON_DISK);
+ }
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestDateTimeUtils.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestDateTimeUtils.java
new file mode 100644
index 0000000..10dc53f
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestDateTimeUtils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.timeline;
+
+import org.joda.time.DateTime;
+import org.joda.time.Seconds;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.meter.MeterTestSuiteNoDB;
+import org.killbill.billing.meter.timeline.util.DateTimeUtils;
+import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
+
+public class TestDateTimeUtils extends MeterTestSuiteNoDB {
+
+ private final Clock clock = new ClockMock();
+
+ @Test(groups = "fast")
+ public void testRoundTrip() throws Exception {
+ final DateTime utcNow = clock.getUTCNow();
+ final int unixSeconds = DateTimeUtils.unixSeconds(utcNow);
+ final DateTime dateTimeFromUnixSeconds = DateTimeUtils.dateTimeFromUnixSeconds(unixSeconds);
+
+ Assert.assertEquals(Seconds.secondsBetween(dateTimeFromUnixSeconds, utcNow).getSeconds(), 0);
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestInMemoryEventHandler.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestInMemoryEventHandler.java
new file mode 100644
index 0000000..f7e3f86
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestInMemoryEventHandler.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.timeline;
+
+import java.io.File;
+import java.util.Map;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.meter.MeterTestSuiteNoDB;
+import org.killbill.billing.meter.timeline.codec.DefaultSampleCoder;
+import org.killbill.billing.meter.timeline.codec.SampleCoder;
+import org.killbill.billing.meter.timeline.persistent.FileBackedBuffer;
+import org.killbill.billing.meter.timeline.persistent.TimelineDao;
+import org.killbill.billing.meter.timeline.times.DefaultTimelineCoder;
+import org.killbill.billing.meter.timeline.times.TimelineCoder;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.util.config.MeterConfig;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import com.google.common.collect.ImmutableMap;
+
+public class TestInMemoryEventHandler extends MeterTestSuiteNoDB {
+
+ private static final UUID HOST_UUID = UUID.randomUUID();
+ private static final String EVENT_TYPE = "eventType";
+ private static final String SAMPLE_KIND_A = "kindA";
+ private static final String SAMPLE_KIND_B = "kindB";
+ private static final Map<String, Object> EVENT = ImmutableMap.<String, Object>of(SAMPLE_KIND_A, 12, SAMPLE_KIND_B, 42);
+ private static final int NB_EVENTS = 5;
+ private static final File basePath = new File(System.getProperty("java.io.tmpdir"), "TestInMemoryCollectorEventProcessor-" + System.currentTimeMillis());
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+ private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+ private final NonEntityDao nonEntityDao = Mockito.mock(NonEntityDao.class);
+ private final InternalCallContextFactory internalCallContextFactory = new InternalCallContextFactory(new ClockMock(), nonEntityDao, new CacheControllerDispatcher());
+
+
+ private final TimelineDao dao = new MockTimelineDao();
+ private TimelineEventHandler timelineEventHandler;
+ private int eventTypeId = 0;
+
+ @BeforeMethod(alwaysRun = true)
+ public void setUp() throws Exception {
+ Assert.assertTrue(basePath.mkdir());
+ System.setProperty("killbill.usage.timelines.spoolDir", basePath.getAbsolutePath());
+ final MeterConfig config = new ConfigurationObjectFactory(System.getProperties()).build(MeterConfig.class);
+ timelineEventHandler = new TimelineEventHandler(config, dao, timelineCoder, sampleCoder, new BackgroundDBChunkWriter(dao, config, internalCallContextFactory),
+ new FileBackedBuffer(config.getSpoolDir(), "TimelineEventHandler", 1024 * 1024, 10));
+
+ dao.getOrAddSource(HOST_UUID.toString(), internalCallContext);
+ eventTypeId = dao.getOrAddEventCategory(EVENT_TYPE, internalCallContext);
+ }
+
+ @Test(groups = "fast")
+ public void testInMemoryFilters() throws Exception {
+ final DateTime startTime = new DateTime(DateTimeZone.UTC);
+ for (int i = 0; i < NB_EVENTS; i++) {
+ timelineEventHandler.record(HOST_UUID.toString(), EVENT_TYPE, startTime, EVENT, internalCallContext);
+ }
+ final DateTime endTime = new DateTime(DateTimeZone.UTC);
+
+ final Integer hostId = dao.getSourceId(HOST_UUID.toString(), internalCallContext);
+ Assert.assertNotNull(hostId);
+ final Integer sampleKindAId = dao.getMetricId(eventTypeId, SAMPLE_KIND_A, internalCallContext);
+ Assert.assertNotNull(sampleKindAId);
+ final Integer sampleKindBId = dao.getMetricId(eventTypeId, SAMPLE_KIND_B, internalCallContext);
+ Assert.assertNotNull(sampleKindBId);
+
+ // One per host and type
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, null, null, internalCallContext).size(), 2);
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, startTime, null, internalCallContext).size(), 2);
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, null, endTime, internalCallContext).size(), 2);
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, startTime, endTime, internalCallContext).size(), 2);
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, sampleKindAId, startTime, endTime, internalCallContext).size(), 1);
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, sampleKindBId, startTime, endTime, internalCallContext).size(), 1);
+ // Wider ranges should be supported
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, sampleKindBId, startTime.minusSeconds(1), endTime, internalCallContext).size(), 1);
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, sampleKindBId, startTime, endTime.plusSeconds(1), internalCallContext).size(), 1);
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, sampleKindBId, startTime.minusSeconds(1), endTime.plusSeconds(1), internalCallContext).size(), 1);
+ // Buggy kind
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, Integer.MAX_VALUE, startTime, endTime, internalCallContext).size(), 0);
+ // Buggy start date
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, startTime.plusMinutes(1), endTime, internalCallContext).size(), 0);
+ // Buggy end date
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(hostId, startTime, endTime.minusMinutes(1), internalCallContext).size(), 0);
+ // Buggy host
+ Assert.assertEquals(timelineEventHandler.getInMemoryTimelineChunks(Integer.MAX_VALUE, startTime, endTime, internalCallContext).size(), 0);
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestTimelineEventHandler.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestTimelineEventHandler.java
new file mode 100644
index 0000000..30b8194
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestTimelineEventHandler.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.timeline;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.mockito.Mockito;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.meter.MeterTestSuiteNoDB;
+import org.killbill.billing.meter.timeline.codec.DefaultSampleCoder;
+import org.killbill.billing.meter.timeline.codec.SampleCoder;
+import org.killbill.billing.meter.timeline.persistent.TimelineDao;
+import org.killbill.billing.meter.timeline.samples.ScalarSample;
+import org.killbill.billing.meter.timeline.sources.SourceSamplesForTimestamp;
+import org.killbill.billing.meter.timeline.times.DefaultTimelineCoder;
+import org.killbill.billing.meter.timeline.times.TimelineCoder;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.util.config.MeterConfig;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import com.google.common.collect.ImmutableMap;
+
+public class TestTimelineEventHandler extends MeterTestSuiteNoDB {
+
+ private static final File basePath = new File(System.getProperty("java.io.tmpdir"), "TestTimelineEventHandler-" + System.currentTimeMillis());
+ private static final String EVENT_TYPE = "eventType";
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+ private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+ private final NonEntityDao nonEntityDao = Mockito.mock(NonEntityDao.class);
+ private final InternalCallContextFactory internalCallContextFactory = new InternalCallContextFactory(new ClockMock(), nonEntityDao, new CacheControllerDispatcher());
+
+ private final TimelineDao dao = new MockTimelineDao();
+
+ @Test(groups = "fast")
+ public void testDownsizingValues() throws Exception {
+ Assert.assertTrue(basePath.mkdir());
+ System.setProperty("killbill.usage.timelines.spoolDir", basePath.getAbsolutePath());
+ final MeterConfig config = new ConfigurationObjectFactory(System.getProperties()).build(MeterConfig.class);
+ final int eventTypeId = dao.getOrAddEventCategory(EVENT_TYPE, internalCallContext);
+ final int int2shortId = dao.getOrAddMetric(eventTypeId, "int2short", internalCallContext);
+ final int long2intId = dao.getOrAddMetric(eventTypeId, "long2int", internalCallContext);
+ final int long2shortId = dao.getOrAddMetric(eventTypeId, "long2short", internalCallContext);
+ final int int2intId = dao.getOrAddMetric(eventTypeId, "int2int", internalCallContext);
+ final int long2longId = dao.getOrAddMetric(eventTypeId, "long2long", internalCallContext);
+ final int hostId = 1;
+ final TimelineEventHandler handler = new TimelineEventHandler(config, dao, timelineCoder, sampleCoder,
+ new BackgroundDBChunkWriter(dao, config, internalCallContextFactory), new MockFileBackedBuffer());
+
+ // Test downsizing of values
+ final Map<String, Object> event = ImmutableMap.<String, Object>of(
+ "int2short", new Integer(1),
+ "long2int", new Long(Integer.MAX_VALUE),
+ "long2short", new Long(2),
+ "int2int", Integer.MAX_VALUE,
+ "long2long", Long.MAX_VALUE);
+ final Map<Integer, ScalarSample> output = convertEventToSamples(handler, event, EVENT_TYPE);
+
+ Assert.assertEquals(output.get(int2shortId).getSampleValue(), (short) 1);
+ Assert.assertEquals(output.get(int2shortId).getSampleValue().getClass(), Short.class);
+ Assert.assertEquals(output.get(long2intId).getSampleValue(), Integer.MAX_VALUE);
+ Assert.assertEquals(output.get(long2intId).getSampleValue().getClass(), Integer.class);
+ Assert.assertEquals(output.get(long2shortId).getSampleValue(), (short) 2);
+ Assert.assertEquals(output.get(long2shortId).getSampleValue().getClass(), Short.class);
+ Assert.assertEquals(output.get(int2intId).getSampleValue(), Integer.MAX_VALUE);
+ Assert.assertEquals(output.get(int2intId).getSampleValue().getClass(), Integer.class);
+ Assert.assertEquals(output.get(long2longId).getSampleValue(), Long.MAX_VALUE);
+ Assert.assertEquals(output.get(long2longId).getSampleValue().getClass(), Long.class);
+ }
+
+ private Map<Integer, ScalarSample> convertEventToSamples(final TimelineEventHandler handler, final Map<String, Object> event, final String eventType) {
+ final Map<Integer, ScalarSample> output = new HashMap<Integer, ScalarSample>();
+ handler.convertSamplesToScalarSamples(eventType, event, output, internalCallContext);
+ return output;
+ }
+
+ private void processOneEvent(final TimelineEventHandler handler, final int hostId, final String eventType, final String sampleKind, final DateTime timestamp) throws Exception {
+ final Map<String, Object> rawEvent = ImmutableMap.<String, Object>of(sampleKind, new Integer(1));
+ final Map<Integer, ScalarSample> convertedEvent = convertEventToSamples(handler, rawEvent, eventType);
+ handler.processSamples(new SourceSamplesForTimestamp(hostId, eventType, timestamp, convertedEvent), internalCallContext);
+ }
+
+ private void sleep(final int millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testPurgeAccumulators() throws Exception {
+ System.setProperty("arecibo.collector.timelines.spoolDir", basePath.getAbsolutePath());
+ final MeterConfig config = new ConfigurationObjectFactory(System.getProperties()).build(MeterConfig.class);
+ final TimelineEventHandler handler = new TimelineEventHandler(config, dao, timelineCoder, sampleCoder, new BackgroundDBChunkWriter(dao, config, internalCallContextFactory), new MockFileBackedBuffer());
+ Assert.assertEquals(handler.getAccumulators().size(), 0);
+ processOneEvent(handler, 1, "eventType1", "sampleKind1", new DateTime());
+ sleep(20);
+ final DateTime purgeBeforeTime = new DateTime();
+ sleep(20);
+ processOneEvent(handler, 1, "eventType2", "sampleKind2", new DateTime());
+ Assert.assertEquals(handler.getAccumulators().size(), 2);
+ handler.purgeOldSourcesAndAccumulators(purgeBeforeTime);
+ final Collection<TimelineSourceEventAccumulator> accumulators = handler.getAccumulators();
+ Assert.assertEquals(accumulators.size(), 1);
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestTimelineSourceEventAccumulator.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestTimelineSourceEventAccumulator.java
new file mode 100644
index 0000000..39b6cf9
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TestTimelineSourceEventAccumulator.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.timeline;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.meter.MeterTestSuiteNoDB;
+import org.killbill.billing.meter.timeline.codec.DefaultSampleCoder;
+import org.killbill.billing.meter.timeline.codec.SampleCoder;
+import org.killbill.billing.meter.timeline.samples.SampleOpcode;
+import org.killbill.billing.meter.timeline.samples.ScalarSample;
+import org.killbill.billing.meter.timeline.sources.SourceSamplesForTimestamp;
+import org.killbill.billing.meter.timeline.times.DefaultTimelineCoder;
+import org.killbill.billing.meter.timeline.times.TimelineCoder;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+public class TestTimelineSourceEventAccumulator extends MeterTestSuiteNoDB {
+
+ private static final int HOST_ID = 1;
+ private static final int EVENT_CATEGORY_ID = 123;
+
+ private static final MockTimelineDao dao = new MockTimelineDao();
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+ private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+ private final NonEntityDao nonEntityDao = Mockito.mock(NonEntityDao.class);
+ private final InternalCallContextFactory internalCallContextFactory = new InternalCallContextFactory(new ClockMock(), nonEntityDao, new CacheControllerDispatcher());
+
+ @Test(groups = "fast")
+ public void testSimpleAggregate() throws IOException {
+ final DateTime startTime = new DateTime(DateTimeZone.UTC);
+ final TimelineSourceEventAccumulator accumulator = new TimelineSourceEventAccumulator(dao, timelineCoder, sampleCoder, HOST_ID,
+ EVENT_CATEGORY_ID, startTime, internalCallContextFactory);
+
+ // Send a first type of data
+ final int sampleCount = 5;
+ final int sampleKindId = 1;
+ sendData(accumulator, startTime, sampleCount, sampleKindId);
+ Assert.assertEquals(accumulator.getStartTime(), startTime);
+ Assert.assertEquals(accumulator.getEndTime(), startTime.plusSeconds(sampleCount - 1));
+ Assert.assertEquals(accumulator.getSourceId(), HOST_ID);
+ Assert.assertEquals(accumulator.getTimelines().size(), 1);
+ Assert.assertEquals(accumulator.getTimelines().get(sampleKindId).getSampleCount(), sampleCount);
+ Assert.assertEquals(accumulator.getTimelines().get(sampleKindId).getMetricId(), sampleKindId);
+
+ // Send now a second type
+ final DateTime secondStartTime = startTime.plusSeconds(sampleCount + 1);
+ final int secondSampleCount = 15;
+ final int secondSampleKindId = 2;
+ sendData(accumulator, secondStartTime, secondSampleCount, secondSampleKindId);
+ // We keep the start time of the accumulator
+ Assert.assertEquals(accumulator.getStartTime(), startTime);
+ Assert.assertEquals(accumulator.getEndTime(), secondStartTime.plusSeconds(secondSampleCount - 1));
+ Assert.assertEquals(accumulator.getSourceId(), HOST_ID);
+ Assert.assertEquals(accumulator.getTimelines().size(), 2);
+ // We advance all timelines in parallel
+ Assert.assertEquals(accumulator.getTimelines().get(sampleKindId).getSampleCount(), sampleCount + secondSampleCount);
+ Assert.assertEquals(accumulator.getTimelines().get(sampleKindId).getMetricId(), sampleKindId);
+ Assert.assertEquals(accumulator.getTimelines().get(secondSampleKindId).getSampleCount(), sampleCount + secondSampleCount);
+ Assert.assertEquals(accumulator.getTimelines().get(secondSampleKindId).getMetricId(), secondSampleKindId);
+ }
+
+ private void sendData(final TimelineSourceEventAccumulator accumulator, final DateTime startTime, final int sampleCount, final int sampleKindId) {
+ final Map<Integer, ScalarSample> samples = new HashMap<Integer, ScalarSample>();
+
+ for (int i = 0; i < sampleCount; i++) {
+ samples.put(sampleKindId, new ScalarSample<Long>(SampleOpcode.LONG, i + 1242L));
+ final SourceSamplesForTimestamp hostSamplesForTimestamp = new SourceSamplesForTimestamp(HOST_ID, "JVM", startTime.plusSeconds(i), samples);
+ accumulator.addSourceSamples(hostSamplesForTimestamp);
+ }
+ }
+}
diff --git a/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TimelineLoadGenerator.java b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TimelineLoadGenerator.java
new file mode 100644
index 0000000..01c88a8
--- /dev/null
+++ b/osgi-bundles/bundles/meter/src/test/java/org/killbill/billing/meter/timeline/TimelineLoadGenerator.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.meter.timeline;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.DBI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.meter.timeline.categories.CategoryRecordIdAndMetric;
+import org.killbill.billing.meter.timeline.chunks.TimelineChunk;
+import org.killbill.billing.meter.timeline.persistent.CachingTimelineDao;
+import org.killbill.billing.meter.timeline.persistent.DefaultTimelineDao;
+import org.killbill.billing.meter.timeline.times.DefaultTimelineCoder;
+import org.killbill.billing.meter.timeline.times.TimelineCoder;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
+
+import com.google.common.collect.BiMap;
+
+/**
+ * This class simulates the database load due to insertions and deletions of
+ * TimelineChunks rows, as required by sample processing and
+ * aggregation. Each is single-threaded.
+ */
+public class TimelineLoadGenerator {
+
+ private static final Logger log = LoggerFactory.getLogger(TimelineLoadGenerator.class);
+ private static final int EVENT_CATEGORY_COUNT = Integer.parseInt(System.getProperty("org.killbill.billing.timeline.eventCategoryCount", "250"));
+ private static final int HOST_ID_COUNT = Integer.parseInt(System.getProperty("org.killbill.billing.timeline.hostIdCount", "2000"));
+ private static final int AVERAGE_SAMPLE_KINDS_PER_CATEGORY = Integer.parseInt(System.getProperty("org.killbill.billing.timeline.averageSampleKindsPerCategory", "20"));
+ private static final int AVERAGE_CATEGORIES_PER_HOST = Integer.parseInt(System.getProperty("org.killbill.billing.timeline.averageSampleKindsPerCategory", "25"));
+ private static final int SAMPLE_KIND_COUNT = EVENT_CATEGORY_COUNT * AVERAGE_SAMPLE_KINDS_PER_CATEGORY;
+ private static final int CREATE_BATCH_SIZE = Integer.parseInt(System.getProperty("org.killbill.billing.timeline.createBatchSize", "1000"));
+ // Mandatory properties
+ private static final String DBI_URL = System.getProperty("org.killbill.billing.timeline.db.url");
+ private static final String DBI_USER = System.getProperty("org.killbill.billing.timeline.db.user");
+ private static final String DBI_PASSWORD = System.getProperty("org.killbill.billing.timeline.db.password");
+
+ private static final Random rand = new Random(System.currentTimeMillis());
+
+ private final List<Integer> hostIds;
+ private final BiMap<Integer, String> hosts;
+ private final BiMap<Integer, String> eventCategories;
+ private final List<Integer> eventCategoryIds;
+ private final BiMap<Integer, CategoryRecordIdAndMetric> sampleKindsBiMap;
+ private final Map<Integer, List<Integer>> categorySampleKindIds;
+ private final Map<Integer, List<Integer>> categoriesForHostId;
+
+ private final DefaultTimelineDao defaultTimelineDAO;
+ private final CachingTimelineDao timelineDAO;
+ private final DBI dbi;
+ private final TimelineCoder timelineCoder;
+
+ private final AtomicInteger timelineChunkIdCounter = new AtomicInteger(0);
+
+ private final Clock clock = new ClockMock();
+ private final InternalCallContext internalCallContext = new InternalCallContext(InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID, 1687L, UUID.randomUUID(),
+ UUID.randomUUID().toString(), CallOrigin.TEST,
+ UserType.TEST, "Testing", "This is a test",
+ clock.getUTCNow(), clock.getUTCNow());
+
+ public TimelineLoadGenerator() {
+ this.timelineCoder = new DefaultTimelineCoder();
+
+ this.dbi = new DBI(DBI_URL, DBI_USER, DBI_PASSWORD);
+ this.defaultTimelineDAO = new DefaultTimelineDao(dbi);
+ this.timelineDAO = new CachingTimelineDao(defaultTimelineDAO);
+ log.info("DBI initialized");
+
+ // Make some hosts
+ final List<String> hostNames = new ArrayList<String>(HOST_ID_COUNT);
+ for (int i = 0; i < HOST_ID_COUNT; i++) {
+ final String hostName = String.format("host-%d", i + 1);
+ hostNames.add(hostName);
+ defaultTimelineDAO.getOrAddSource(hostName, internalCallContext);
+ }
+ hosts = timelineDAO.getSources(internalCallContext);
+ hostIds = new ArrayList<Integer>(hosts.keySet());
+ Collections.sort(hostIds);
+ log.info("%d hosts created", hostIds.size());
+
+ // Make some event categories
+ final List<String> categoryNames = new ArrayList<String>(EVENT_CATEGORY_COUNT);
+ for (int i = 0; i < EVENT_CATEGORY_COUNT; i++) {
+ final String category = String.format("category-%d", i);
+ categoryNames.add(category);
+ defaultTimelineDAO.getOrAddEventCategory(category, internalCallContext);
+ }
+ eventCategories = timelineDAO.getEventCategories(internalCallContext);
+ eventCategoryIds = new ArrayList<Integer>(eventCategories.keySet());
+ Collections.sort(eventCategoryIds);
+ log.info("%d event categories created", eventCategoryIds.size());
+
+ // Make some sample kinds. For now, give each category the same number of sample kinds
+ final List<CategoryRecordIdAndMetric> categoriesAndSampleKinds = new ArrayList<CategoryRecordIdAndMetric>();
+ for (final int eventCategoryId : eventCategoryIds) {
+ for (int i = 0; i < AVERAGE_SAMPLE_KINDS_PER_CATEGORY; i++) {
+ final String sampleKind = String.format("%s-sample-kind-%d", eventCategories.get(eventCategoryId), i + 1);
+ categoriesAndSampleKinds.add(new CategoryRecordIdAndMetric(eventCategoryId, sampleKind));
+ defaultTimelineDAO.getOrAddMetric(eventCategoryId, sampleKind, internalCallContext);
+ }
+ }
+ // Make a fast map from categoryId to a list of sampleKindIds in that category
+ sampleKindsBiMap = timelineDAO.getMetrics(internalCallContext);
+ categorySampleKindIds = new HashMap<Integer, List<Integer>>();
+ int sampleKindIdCounter = 0;
+ for (final Map.Entry<Integer, CategoryRecordIdAndMetric> entry : sampleKindsBiMap.entrySet()) {
+ final int categoryId = entry.getValue().getEventCategoryId();
+ List<Integer> sampleKindIds = categorySampleKindIds.get(categoryId);
+ if (sampleKindIds == null) {
+ sampleKindIds = new ArrayList<Integer>();
+ categorySampleKindIds.put(categoryId, sampleKindIds);
+ }
+ final int sampleKindId = entry.getKey();
+ sampleKindIds.add(sampleKindId);
+ sampleKindIdCounter++;
+ }
+ log.info("%d sampleKindIds created", sampleKindIdCounter);
+ // Assign categories to hosts
+ categoriesForHostId = new HashMap<Integer, List<Integer>>();
+ int categoryCounter = 0;
+ for (final int hostId : hostIds) {
+ final List<Integer> categories = new ArrayList<Integer>();
+ categoriesForHostId.put(hostId, categories);
+ for (int i = 0; i < AVERAGE_CATEGORIES_PER_HOST; i++) {
+ final int categoryId = eventCategoryIds.get(categoryCounter);
+ categories.add(categoryId);
+ categoryCounter = (categoryCounter + 1) % EVENT_CATEGORY_COUNT;
+ }
+ }
+ log.info("Finished creating hosts, categories and sample kinds");
+ }
+
+ private void addChunkAndMaybeSave(final List<TimelineChunk> timelineChunkList, final TimelineChunk timelineChunk) {
+ timelineChunkList.add(timelineChunk);
+ if (timelineChunkList.size() >= CREATE_BATCH_SIZE) {
+ defaultTimelineDAO.bulkInsertTimelineChunks(timelineChunkList, internalCallContext);
+ timelineChunkList.clear();
+ log.info("Inserted %d TimelineChunk rows", timelineChunkIdCounter.get());
+ }
+ }
+
+ /**
+ * This method simulates adding a ton of timelines, in more-or-less the way they would be added in real life.
+ */
+ private void insertManyTimelines() throws Exception {
+ final List<TimelineChunk> timelineChunkList = new ArrayList<TimelineChunk>();
+ DateTime startTime = new DateTime().minusDays(1);
+ DateTime endTime = startTime.plusHours(1);
+ final int sampleCount = 120; // 1 hours worth
+ for (int i = 0; i < 12; i++) {
+ for (final int hostId : hostIds) {
+ for (final int categoryId : categoriesForHostId.get(hostId)) {
+ final List<DateTime> dateTimes = new ArrayList<DateTime>(sampleCount);
+ for (int sc = 0; sc < sampleCount; sc++) {
+ dateTimes.add(startTime.plusSeconds(sc * 30));
+ }
+ final byte[] timeBytes = timelineCoder.compressDateTimes(dateTimes);
+ for (final int sampleKindId : categorySampleKindIds.get(categoryId)) {
+ final TimelineChunk timelineChunk = makeTimelineChunk(hostId, sampleKindId, startTime, endTime, timeBytes, sampleCount);
+ addChunkAndMaybeSave(timelineChunkList, timelineChunk);
+
+ }
+ }
+ }
+ if (timelineChunkList.size() > 0) {
+ defaultTimelineDAO.bulkInsertTimelineChunks(timelineChunkList, internalCallContext);
+ }
+ log.info("After hour %d, inserted %d TimelineChunk rows", i, timelineChunkIdCounter.get());
+ startTime = endTime;
+ endTime = endTime.plusHours(1);
+ }
+ }
+
+ private TimelineChunk makeTimelineChunk(final int hostId, final int sampleKindId, final DateTime startTime, final DateTime endTime, final byte[] timeBytes, final int sampleCount) {
+ final byte[] samples = new byte[3 + rand.nextInt(sampleCount) * 2];
+ return new TimelineChunk(timelineChunkIdCounter.incrementAndGet(), hostId, sampleKindId, startTime, endTime, timeBytes, samples, sampleCount);
+ }
+
+ public static void main(final String[] args) throws Exception {
+ final TimelineLoadGenerator loadGenerator = new TimelineLoadGenerator();
+ loadGenerator.insertManyTimelines();
+ }
+}
osgi-bundles/bundles/pom.xml 4(+2 -2)
diff --git a/osgi-bundles/bundles/pom.xml b/osgi-bundles/bundles/pom.xml
index 17a5e93..1360266 100644
--- a/osgi-bundles/bundles/pom.xml
+++ b/osgi-bundles/bundles/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-all-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-bundles</artifactId>
diff --git a/osgi-bundles/bundles/webconsolebranding/pom.xml b/osgi-bundles/bundles/webconsolebranding/pom.xml
index 1fb1a57..e713012 100644
--- a/osgi-bundles/bundles/webconsolebranding/pom.xml
+++ b/osgi-bundles/bundles/webconsolebranding/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-bundles-webconsolebranding</artifactId>
osgi-bundles/defaultbundles/pom.xml 28(+14 -14)
diff --git a/osgi-bundles/defaultbundles/pom.xml b/osgi-bundles/defaultbundles/pom.xml
index 7b7f905..910a099 100644
--- a/osgi-bundles/defaultbundles/pom.xml
+++ b/osgi-bundles/defaultbundles/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-all-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-bundles-defaultbundles</artifactId>
@@ -27,18 +27,6 @@
<name>Killbill billing platform: OSGI default bundles</name>
<dependencies>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-osgi-bundles-jruby</artifactId>
- </dependency>
- <dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-osgi-bundles-logger</artifactId>
- </dependency>
- <dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-osgi-bundles-webconsolebranding</artifactId>
- </dependency>
- <dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.bundlerepository</artifactId>
<version>1.6.6</version>
@@ -128,6 +116,18 @@
<artifactId>org.apache.felix.webconsole</artifactId>
<version>3.1.8</version>
</dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-osgi-bundles-jruby</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-osgi-bundles-logger</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-osgi-bundles-webconsolebranding</artifactId>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git a/osgi-bundles/defaultbundles/src/main/assembly/assembly.xml b/osgi-bundles/defaultbundles/src/main/assembly/assembly.xml
index 9ffda42..168ac0d 100644
--- a/osgi-bundles/defaultbundles/src/main/assembly/assembly.xml
+++ b/osgi-bundles/defaultbundles/src/main/assembly/assembly.xml
@@ -14,7 +14,7 @@
<useTransitiveDependencies>false</useTransitiveDependencies>
<unpack>false</unpack>
<excludes>
- <exclude>com.ning.billing:killbill-osgi-bundles-jruby:jar</exclude>
+ <exclude>org.killbill.billing:killbill-osgi-bundles-jruby:jar</exclude>
</excludes>
</dependencySet>
<dependencySet>
@@ -25,7 +25,7 @@
<useTransitiveDependencies>false</useTransitiveDependencies>
<unpack>false</unpack>
<includes>
- <include>com.ning.billing:killbill-osgi-bundles-jruby:jar</include>
+ <include>org.killbill.billing:killbill-osgi-bundles-jruby:jar</include>
</includes>
</dependencySet>
</dependencySets>
osgi-bundles/libs/killbill/pom.xml 16(+8 -8)
diff --git a/osgi-bundles/libs/killbill/pom.xml b/osgi-bundles/libs/killbill/pom.xml
index a391e53..b084407 100644
--- a/osgi-bundles/libs/killbill/pom.xml
+++ b/osgi-bundles/libs/killbill/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-lib-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-bundles-lib-killbill</artifactId>
@@ -33,18 +33,18 @@
<scope>compile</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-api</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
- <artifactId>killbill-plugin-api-notification</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>javax.servlet</groupId>
- <artifactId>javax.servlet-api</artifactId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
+ <artifactId>killbill-plugin-api-notification</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
diff --git a/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/KillbillActivatorBase.java b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/KillbillActivatorBase.java
new file mode 100644
index 0000000..2541c43
--- /dev/null
+++ b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/KillbillActivatorBase.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.killbill.osgi.libs.killbill;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillEventDispatcher.OSGIKillbillEventHandler;
+
+public abstract class KillbillActivatorBase implements BundleActivator {
+
+
+ protected OSGIKillbillAPI killbillAPI;
+ protected OSGIKillbillLogService logService;
+ protected OSGIKillbillRegistrar registrar;
+ protected OSGIKillbillDataSource dataSource;
+ protected OSGIKillbillEventDispatcher dispatcher;
+
+ @Override
+ public void start(final BundleContext context) throws Exception {
+
+ // Tracked resource
+ killbillAPI = new OSGIKillbillAPI(context);
+ logService = new OSGIKillbillLogService(context);
+ dataSource = new OSGIKillbillDataSource(context);
+ dispatcher = new OSGIKillbillEventDispatcher(context);
+
+ // Registrar for bundle
+ registrar = new OSGIKillbillRegistrar();
+
+ // Killbill events
+ final OSGIKillbillEventHandler handler = getOSGIKillbillEventHandler();
+ if (handler != null) {
+ dispatcher.registerEventHandler(handler);
+ }
+ }
+
+ @Override
+ public void stop(final BundleContext context) throws Exception {
+
+ // Close trackers
+ if (killbillAPI != null) {
+ killbillAPI.close();
+ killbillAPI = null;
+ }
+ if (dispatcher != null) {
+ dispatcher.close();
+ dispatcher = null;
+ }
+ if (dataSource != null) {
+ dataSource.close();
+ dataSource = null;
+ }
+ if (logService != null) {
+ logService.close();
+ logService = null;
+ }
+
+ try {
+ // Remove Killbill event handler
+ final OSGIKillbillEventHandler handler = getOSGIKillbillEventHandler();
+ if (handler != null && dispatcher != null) {
+ dispatcher.unregisterEventHandler(handler);
+ dispatcher = null;
+ }
+ } catch (OSGIServiceNotAvailable ignore) {
+ // If the system bundle shut down prior to that bundle, we can' unregister our Observer, which is fine.
+ }
+
+ // Unregister all servies from that bundle
+ if (registrar != null) {
+ registrar.unregisterAll();
+ registrar = null;
+ }
+ }
+
+
+ public abstract OSGIKillbillEventHandler getOSGIKillbillEventHandler();
+}
diff --git a/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillAPI.java b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillAPI.java
new file mode 100644
index 0000000..23928aa
--- /dev/null
+++ b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillAPI.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.killbill.osgi.libs.killbill;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.util.tracker.ServiceTracker;
+
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.catalog.api.CatalogUserApi;
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.entitlement.api.EntitlementApi;
+import org.killbill.billing.entitlement.api.SubscriptionApi;
+import org.killbill.billing.invoice.api.InvoicePaymentApi;
+import org.killbill.billing.invoice.api.InvoiceUserApi;
+import org.killbill.billing.osgi.api.OSGIKillbill;
+import org.killbill.billing.osgi.api.config.PluginConfigServiceApi;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.tenant.api.TenantUserApi;
+import org.killbill.billing.usage.api.UsageUserApi;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.ExportUserApi;
+import org.killbill.billing.util.api.RecordIdApi;
+import org.killbill.billing.util.api.TagUserApi;
+
+public class OSGIKillbillAPI extends OSGIKillbillLibraryBase implements OSGIKillbill {
+
+
+ private static final String KILLBILL_SERVICE_NAME = "org.killbill.billing.osgi.api.OSGIKillbill";
+
+ private final ServiceTracker<OSGIKillbill, OSGIKillbill> killbillTracker;
+
+ public OSGIKillbillAPI(BundleContext context) {
+ killbillTracker = new ServiceTracker(context, KILLBILL_SERVICE_NAME, null);
+ killbillTracker.open();
+ }
+
+ public void close() {
+ if (killbillTracker != null) {
+ killbillTracker.close();
+ }
+ }
+
+ @Override
+ public AccountUserApi getAccountUserApi() {
+ return withServiceTracker(killbillTracker, new APICallback<AccountUserApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public AccountUserApi executeWithService(final OSGIKillbill service) {
+ return service.getAccountUserApi();
+ }
+ });
+ }
+
+ @Override
+ public CatalogUserApi getCatalogUserApi() {
+ return withServiceTracker(killbillTracker, new APICallback<CatalogUserApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public CatalogUserApi executeWithService(final OSGIKillbill service) {
+ return service.getCatalogUserApi();
+ }
+ });
+ }
+
+ @Override
+ public SubscriptionApi getSubscriptionApi() {
+ return withServiceTracker(killbillTracker, new APICallback<SubscriptionApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public SubscriptionApi executeWithService(final OSGIKillbill service) {
+ return service.getSubscriptionApi();
+ }
+ });
+ }
+
+
+ @Override
+ public InvoicePaymentApi getInvoicePaymentApi() {
+ return withServiceTracker(killbillTracker, new APICallback<InvoicePaymentApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public InvoicePaymentApi executeWithService(final OSGIKillbill service) {
+ return service.getInvoicePaymentApi();
+ }
+ });
+ }
+
+ @Override
+ public InvoiceUserApi getInvoiceUserApi() {
+ return withServiceTracker(killbillTracker, new APICallback<InvoiceUserApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public InvoiceUserApi executeWithService(final OSGIKillbill service) {
+ return service.getInvoiceUserApi();
+ }
+ });
+ }
+
+ @Override
+ public PaymentApi getPaymentApi() {
+ return withServiceTracker(killbillTracker, new APICallback<PaymentApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public PaymentApi executeWithService(final OSGIKillbill service) {
+ return service.getPaymentApi();
+ }
+ });
+ }
+
+ @Override
+ public TenantUserApi getTenantUserApi() {
+ return withServiceTracker(killbillTracker, new APICallback<TenantUserApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public TenantUserApi executeWithService(final OSGIKillbill service) {
+ return service.getTenantUserApi();
+ }
+ });
+ }
+
+ @Override
+ public UsageUserApi getUsageUserApi() {
+ return withServiceTracker(killbillTracker, new APICallback<UsageUserApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public UsageUserApi executeWithService(final OSGIKillbill service) {
+ return service.getUsageUserApi();
+ }
+ });
+ }
+
+ @Override
+ public AuditUserApi getAuditUserApi() {
+ return withServiceTracker(killbillTracker, new APICallback<AuditUserApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public AuditUserApi executeWithService(final OSGIKillbill service) {
+ return service.getAuditUserApi();
+ }
+ });
+ }
+
+ @Override
+ public CustomFieldUserApi getCustomFieldUserApi() {
+ return withServiceTracker(killbillTracker, new APICallback<CustomFieldUserApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public CustomFieldUserApi executeWithService(final OSGIKillbill service) {
+ return service.getCustomFieldUserApi();
+ }
+ });
+ }
+
+ @Override
+ public ExportUserApi getExportUserApi() {
+ return withServiceTracker(killbillTracker, new APICallback<ExportUserApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public ExportUserApi executeWithService(final OSGIKillbill service) {
+ return service.getExportUserApi();
+ }
+ });
+ }
+
+ @Override
+ public TagUserApi getTagUserApi() {
+ return withServiceTracker(killbillTracker, new APICallback<TagUserApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public TagUserApi executeWithService(final OSGIKillbill service) {
+ return service.getTagUserApi();
+ }
+ });
+ }
+
+ @Override
+ public EntitlementApi getEntitlementApi() {
+ return withServiceTracker(killbillTracker, new APICallback<EntitlementApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public EntitlementApi executeWithService(final OSGIKillbill service) {
+ return service.getEntitlementApi();
+ }
+ });
+ }
+
+ @Override
+ public RecordIdApi getRecordIdApi() {
+ return withServiceTracker(killbillTracker, new APICallback<RecordIdApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public RecordIdApi executeWithService(final OSGIKillbill service) {
+ return service.getRecordIdApi();
+ }
+ });
+ }
+
+ @Override
+ public CurrencyConversionApi getCurrencyConversionApi() {
+ return withServiceTracker(killbillTracker, new APICallback<CurrencyConversionApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public CurrencyConversionApi executeWithService(final OSGIKillbill service) {
+ return service.getCurrencyConversionApi();
+ }
+ });
+ }
+
+ @Override
+ public PluginConfigServiceApi getPluginConfigServiceApi() {
+ return withServiceTracker(killbillTracker, new APICallback<PluginConfigServiceApi, OSGIKillbill>(KILLBILL_SERVICE_NAME) {
+ @Override
+ public PluginConfigServiceApi executeWithService(final OSGIKillbill service) {
+ return service.getPluginConfigServiceApi();
+ }
+ });
+ }
+}
diff --git a/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillDataSource.java b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillDataSource.java
new file mode 100644
index 0000000..e23223f
--- /dev/null
+++ b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillDataSource.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.killbill.osgi.libs.killbill;
+
+import javax.sql.DataSource;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class OSGIKillbillDataSource extends OSGIKillbillLibraryBase {
+
+ private static final String DATASOURCE_SERVICE_NAME = "javax.sql.DataSource";
+
+ private final ServiceTracker<DataSource, DataSource> dataSourceTracker;
+
+
+ public OSGIKillbillDataSource(BundleContext context) {
+ dataSourceTracker = new ServiceTracker(context, DATASOURCE_SERVICE_NAME, null);
+ dataSourceTracker.open();
+ }
+
+ public void close() {
+ if (dataSourceTracker != null) {
+ dataSourceTracker.close();
+ }
+ }
+
+ public DataSource getDataSource() {
+ return withServiceTracker(dataSourceTracker, new APICallback<DataSource, DataSource>(DATASOURCE_SERVICE_NAME) {
+ @Override
+ public DataSource executeWithService(final DataSource service) {
+ return dataSourceTracker.getService();
+ }
+ });
+ }
+}
diff --git a/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillEventDispatcher.java b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillEventDispatcher.java
new file mode 100644
index 0000000..488c235
--- /dev/null
+++ b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillEventDispatcher.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.killbill.osgi.libs.killbill;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Observable;
+import java.util.Observer;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.util.tracker.ServiceTracker;
+
+import org.killbill.billing.notification.plugin.api.ExtBusEvent;
+
+public class OSGIKillbillEventDispatcher extends OSGIKillbillLibraryBase {
+
+ private static final String OBSERVABLE_SERVICE_NAME = "java.util.Observable";
+
+ private final ServiceTracker<Observable, Observable> observableTracker;
+
+
+ private final Map<OSGIKillbillEventHandler, Observer> handlerToObserver;
+
+ public OSGIKillbillEventDispatcher(BundleContext context) {
+ handlerToObserver = new HashMap<OSGIKillbillEventHandler, Observer>();
+ observableTracker = new ServiceTracker(context, OBSERVABLE_SERVICE_NAME, null);
+ observableTracker.open();
+ }
+
+ public void close() {
+ if (observableTracker != null) {
+ observableTracker.close();
+ }
+ handlerToObserver.clear();
+ }
+
+ public void registerEventHandler(final OSGIKillbillEventHandler handler) {
+
+ withServiceTracker(observableTracker, new APICallback<Void, Observable>(OBSERVABLE_SERVICE_NAME) {
+ @Override
+ public Void executeWithService(final Observable service) {
+
+ final Observer observer = new Observer() {
+ @Override
+ public void update(final Observable o, final Object arg) {
+ if (!(arg instanceof ExtBusEvent)) {
+ // TODO STEPH or should we throw because that should not happen
+ return;
+ }
+ handler.handleKillbillEvent((ExtBusEvent) arg);
+ }
+ };
+ handlerToObserver.put(handler, observer);
+ service.addObserver(observer);
+ return null;
+ }
+ });
+ }
+
+ public void unregisterEventHandler(final OSGIKillbillEventHandler handler) {
+ withServiceTracker(observableTracker, new APICallback<Void, Observable>(OBSERVABLE_SERVICE_NAME) {
+ @Override
+ public Void executeWithService(final Observable service) {
+
+ final Observer observer = handlerToObserver.get(handler);
+ if (observer != null) {
+ service.deleteObserver(observer);
+ handlerToObserver.remove(handler);
+ }
+ return null;
+ }
+ });
+
+ }
+
+ public interface OSGIKillbillEventHandler {
+
+ public void handleKillbillEvent(final ExtBusEvent killbillEvent);
+ }
+
+}
diff --git a/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillLibraryBase.java b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillLibraryBase.java
new file mode 100644
index 0000000..637a278
--- /dev/null
+++ b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillLibraryBase.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.killbill.osgi.libs.killbill;
+
+import org.osgi.util.tracker.ServiceTracker;
+
+public abstract class OSGIKillbillLibraryBase {
+
+
+ public OSGIKillbillLibraryBase() {
+
+ }
+
+ public abstract void close();
+
+
+ protected abstract class APICallback<API, T> {
+
+ private final String serviceName;
+
+ protected APICallback(final String serviceName) {
+ this.serviceName = serviceName;
+ }
+
+ public abstract API executeWithService(T service);
+
+ protected API executeWithNoService() {
+ throw new OSGIServiceNotAvailable(serviceName);
+ }
+ }
+
+ protected <API, S, T> API withServiceTracker(ServiceTracker<S, T> t, APICallback<API, T> cb) {
+ T service = t.getService();
+ if (service == null) {
+ return cb.executeWithNoService();
+ }
+ return cb.executeWithService(service);
+ }
+}
diff --git a/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillLogService.java b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillLogService.java
new file mode 100644
index 0000000..e84c80a
--- /dev/null
+++ b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillLogService.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.killbill.osgi.libs.killbill;
+
+import javax.annotation.Nullable;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogService;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class OSGIKillbillLogService extends OSGIKillbillLibraryBase implements LogService {
+
+ private static final String LOG_SERVICE_NAME = "org.osgi.service.log.LogService";
+
+ private final ServiceTracker<LogService, LogService> logTracker;
+
+
+ public OSGIKillbillLogService(BundleContext context) {
+ super();
+ logTracker = new ServiceTracker(context, LOG_SERVICE_NAME, null);
+ logTracker.open();
+ }
+
+ public void close() {
+ if (logTracker != null) {
+ logTracker.close();
+ }
+ }
+
+ @Override
+ public void log(final int level, final String message) {
+ logInternal(level, message, null);
+ }
+
+ @Override
+ public void log(final int level, final String message, final Throwable exception) {
+ logInternal(level, message, exception);
+ }
+
+ @Override
+ public void log(final ServiceReference sr, final int level, final String message) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void log(final ServiceReference sr, final int level, final String message, final Throwable exception) {
+ throw new UnsupportedOperationException();
+ }
+
+ private void logInternal(final int level, final String message, @Nullable final Throwable t) {
+
+ withServiceTracker(logTracker, new APICallback<Void, LogService>(LOG_SERVICE_NAME) {
+ @Override
+ public Void executeWithService(final LogService service) {
+ if (t == null) {
+ service.log(level, message);
+ } else {
+ service.log(level, message, t);
+ }
+ return null;
+ }
+
+ protected Void executeWithNoService() {
+ if (level >= 2) {
+ System.out.println(message);
+ } else {
+ System.err.println(message);
+ }
+ if (t != null) {
+ t.printStackTrace(System.err);
+ }
+ return null;
+ }
+ });
+ }
+}
diff --git a/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillRegistrar.java b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillRegistrar.java
new file mode 100644
index 0000000..cc53aca
--- /dev/null
+++ b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIKillbillRegistrar.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.killbill.osgi.libs.killbill;
+
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+public class OSGIKillbillRegistrar {
+
+ private final Map<String, ServiceRegistration> serviceRegistrations;
+
+ public OSGIKillbillRegistrar() {
+ this.serviceRegistrations = new HashMap<String, ServiceRegistration>();
+ }
+
+ public <S> void registerService(final BundleContext context, final Class<S> svcClass, final S service, final Dictionary props) {
+ ServiceRegistration svcRegistration = context.registerService(svcClass.getName(), service, props);
+ serviceRegistrations.put(svcClass.getName(), svcRegistration);
+ }
+
+ public <S> void unregisterService(final Class<S> svcClass) {
+ ServiceRegistration svc = serviceRegistrations.remove(svcClass.getName());
+ if (svc != null) {
+ svc.unregister();
+ }
+ }
+
+ public void unregisterAll() {
+ for (ServiceRegistration cur : serviceRegistrations.values()) {
+ cur.unregister();
+ }
+ serviceRegistrations.clear();
+ }
+}
diff --git a/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIServiceNotAvailable.java b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIServiceNotAvailable.java
new file mode 100644
index 0000000..f9b222e
--- /dev/null
+++ b/osgi-bundles/libs/killbill/src/main/java/org/killbill/killbill/osgi/libs/killbill/OSGIServiceNotAvailable.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.killbill.osgi.libs.killbill;
+
+public class OSGIServiceNotAvailable extends RuntimeException {
+
+ private static final String FORMAT_SERVICE_NOT_AVAILABLE = "OSGI service %s is not available";
+
+ public OSGIServiceNotAvailable(String serviceName) {
+ super(toFormat(serviceName));
+ }
+
+ public OSGIServiceNotAvailable(String serviceName, Throwable cause) {
+ super(toFormat(serviceName), cause);
+ }
+
+ public OSGIServiceNotAvailable(Throwable cause) {
+ super(cause);
+ }
+
+ private static String toFormat(String serviceName) {
+ return String.format(FORMAT_SERVICE_NOT_AVAILABLE, serviceName);
+ }
+}
osgi-bundles/libs/pom.xml 4(+2 -2)
diff --git a/osgi-bundles/libs/pom.xml b/osgi-bundles/libs/pom.xml
index 7c28d3b..d0d7158 100644
--- a/osgi-bundles/libs/pom.xml
+++ b/osgi-bundles/libs/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-all-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-lib-bundles</artifactId>
osgi-bundles/libs/slf4j-osgi/pom.xml 6(+3 -3)
diff --git a/osgi-bundles/libs/slf4j-osgi/pom.xml b/osgi-bundles/libs/slf4j-osgi/pom.xml
index c913e53..4527b4d 100644
--- a/osgi-bundles/libs/slf4j-osgi/pom.xml
+++ b/osgi-bundles/libs/slf4j-osgi/pom.xml
@@ -18,15 +18,15 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-lib-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-bundles-lib-slf4j-osgi</artifactId>
<name>Killbill billing platform: OSGI slf4j-osgi Library</name>
<dependencies>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-osgi-bundles-lib-killbill</artifactId>
</dependency>
<dependency>
diff --git a/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/OSGISlf4jLoggerAdapter.java b/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/OSGISlf4jLoggerAdapter.java
index a8b5a96..a23565f 100644
--- a/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/OSGISlf4jLoggerAdapter.java
+++ b/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/OSGISlf4jLoggerAdapter.java
@@ -19,7 +19,7 @@ package org.slf4j.impl;
import org.osgi.service.log.LogService;
import org.slf4j.spi.LocationAwareLogger;
-import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
+import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillLogService;
public final class OSGISlf4jLoggerAdapter extends SimpleLogger {
diff --git a/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/OSGISlf4jLoggerFactory.java b/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/OSGISlf4jLoggerFactory.java
index d111958..f4195de 100644
--- a/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/OSGISlf4jLoggerFactory.java
+++ b/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/OSGISlf4jLoggerFactory.java
@@ -22,7 +22,7 @@ import java.util.Map;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
-import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
+import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillLogService;
public class OSGISlf4jLoggerFactory implements ILoggerFactory {
diff --git a/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/StaticLoggerBinder.java b/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/StaticLoggerBinder.java
index 764ce79..a9d61f0 100644
--- a/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/StaticLoggerBinder.java
+++ b/osgi-bundles/libs/slf4j-osgi/src/main/java/org/slf4j/impl/StaticLoggerBinder.java
@@ -19,7 +19,7 @@ package org.slf4j.impl;
import org.slf4j.ILoggerFactory;
import org.slf4j.spi.LoggerFactoryBinder;
-import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
+import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillLogService;
public class StaticLoggerBinder implements LoggerFactoryBinder {
osgi-bundles/pom.xml 4(+2 -2)
diff --git a/osgi-bundles/pom.xml b/osgi-bundles/pom.xml
index f0e3316..d0f60ec 100644
--- a/osgi-bundles/pom.xml
+++ b/osgi-bundles/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-all-bundles</artifactId>
osgi-bundles/tests/beatrix/pom.xml 24(+12 -12)
diff --git a/osgi-bundles/tests/beatrix/pom.xml b/osgi-bundles/tests/beatrix/pom.xml
index 07e3121..f1bccdf 100644
--- a/osgi-bundles/tests/beatrix/pom.xml
+++ b/osgi-bundles/tests/beatrix/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-test-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-bundles-test-beatrix</artifactId>
@@ -27,26 +27,26 @@
<name>Killbill billing platform: OSGI Beatrix Test bundle</name>
<dependencies>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-osgi-bundles-lib-killbill</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-notification</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
<artifactId>killbill-plugin-api-payment</artifactId>
</dependency>
<dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
@@ -85,10 +85,10 @@
</executions>
<configuration>
<instructions>
- <Bundle-Activator>com.ning.billing.osgi.bundles.test.TestActivator</Bundle-Activator>
+ <Bundle-Activator>org.killbill.billing.osgi.bundles.test.TestActivator</Bundle-Activator>
<Import-Package>
<!-- maven-bundle-plugin does not seem to detect that the library is using OSGIKillbill, this is annoying... -->
- *;resolution:=optional,com.ning.billing.osgi.api
+ *;resolution:=optional,org.killbill.billing.osgi.api
</Import-Package>
</instructions>
</configuration>
diff --git a/osgi-bundles/tests/beatrix/src/main/java/org/killbill/billing/osgi/bundles/test/Dummy.java b/osgi-bundles/tests/beatrix/src/main/java/org/killbill/billing/osgi/bundles/test/Dummy.java
new file mode 100644
index 0000000..255e48e
--- /dev/null
+++ b/osgi-bundles/tests/beatrix/src/main/java/org/killbill/billing/osgi/bundles/test/Dummy.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.test;
+
+public class Dummy {
+
+}
diff --git a/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/dao/TestDao.java b/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/dao/TestDao.java
new file mode 100644
index 0000000..4c5626a
--- /dev/null
+++ b/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/dao/TestDao.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.test.dao;
+
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.TransactionCallback;
+import org.skife.jdbi.v2.TransactionStatus;
+
+public class TestDao {
+
+ private final IDBI dbi;
+
+ public TestDao(final IDBI dbi) {
+ this.dbi = dbi;
+ }
+
+ public void createTable() {
+
+ dbi.inTransaction(new TransactionCallback<Object>() {
+ @Override
+ public Object inTransaction(final Handle conn, final TransactionStatus status) throws Exception {
+ conn.execute("DROP TABLE IF EXISTS test_bundle;");
+ conn.execute("CREATE TABLE test_bundle (" +
+ "record_id int(11) unsigned NOT NULL AUTO_INCREMENT, " +
+ "is_started bool DEFAULT false, " +
+ "is_logged bool DEFAULT false, " +
+ "external_key varchar(128) NULL, " +
+ "payment_id char(36) NULL," +
+ "payment_method_id char(36) NULL," +
+ "payment_amount decimal(10,4) NULL," +
+ "PRIMARY KEY(record_id)" +
+ ");");
+ return null;
+ }
+ });
+ }
+
+ public void insertStarted() {
+ dbi.inTransaction(new TransactionCallback<Object>() {
+ @Override
+ public Object inTransaction(final Handle conn, final TransactionStatus status) throws Exception {
+ conn.execute("INSERT INTO test_bundle (is_started) VALUES (1);");
+ return null;
+ }
+ });
+ }
+
+ public void insertAccountExternalKey(final String externalKey) {
+ dbi.inTransaction(new TransactionCallback<Object>() {
+ @Override
+ public Object inTransaction(final Handle conn, final TransactionStatus status) throws Exception {
+ conn.execute("UPDATE test_bundle SET external_key = '" + externalKey + "' WHERE record_id = 1;");
+ return null;
+ }
+ });
+ }
+
+ public void insertProcessedPayment(final UUID paymentId, final UUID paymentMethodId, final BigDecimal paymentAmount) {
+ dbi.inTransaction(new TransactionCallback<Object>() {
+ @Override
+ public Object inTransaction(final Handle conn, final TransactionStatus status) throws Exception {
+ conn.execute("UPDATE test_bundle SET payment_id = '" + paymentId.toString() +
+ "', payment_method_id = '" + paymentMethodId.toString() +
+ "', payment_amount = " + paymentAmount +
+ " WHERE record_id = 1;");
+ return null;
+ }
+ });
+ }
+}
diff --git a/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestActivator.java b/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestActivator.java
new file mode 100644
index 0000000..0f0952b
--- /dev/null
+++ b/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestActivator.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.test;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.UUID;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.log.LogService;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.notification.plugin.api.ExtBusEvent;
+import org.killbill.billing.notification.plugin.api.ExtBusEventType;
+import org.killbill.billing.osgi.api.OSGIPluginProperties;
+import org.killbill.billing.osgi.bundles.test.dao.TestDao;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.killbill.osgi.libs.killbill.KillbillActivatorBase;
+import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillEventDispatcher.OSGIKillbillEventHandler;
+
+/**
+ * Test class used by Beatrix OSGI test to verify that:
+ * - "test" bundle is started
+ * - test bundle is able to make API call
+ * - test bundle is able to register a fake PaymentApi service
+ * - test bundle can use the DataSource from Killbill and write on disk
+ */
+public class TestActivator extends KillbillActivatorBase implements OSGIKillbillEventHandler {
+
+ private TestDao testDao;
+
+ @Override
+ public void start(final BundleContext context) throws Exception {
+ super.start(context);
+
+ final String bundleName = context.getBundle().getSymbolicName();
+ logService.log(LogService.LOG_INFO, "TestActivator starting bundle = " + bundleName);
+
+ final IDBI dbi = new DBI(dataSource.getDataSource());
+ testDao = new TestDao(dbi);
+ testDao.createTable();
+ testDao.insertStarted();
+ registerPaymentApi(context, testDao);
+ }
+
+ @Override
+ public void stop(final BundleContext context) throws Exception {
+ super.stop(context);
+ System.out.println("Good bye world from TestActivator!");
+ }
+
+ @Override
+ public OSGIKillbillEventHandler getOSGIKillbillEventHandler() {
+ return this;
+ }
+
+ private void registerPaymentApi(final BundleContext context, final TestDao dao) {
+ final Dictionary props = new Hashtable();
+ props.put(OSGIPluginProperties.PLUGIN_NAME_PROP, "test");
+ registrar.registerService(context, PaymentPluginApi.class, new TestPaymentPluginApi("test", dao), props);
+ }
+
+ @Override
+ public void handleKillbillEvent(final ExtBusEvent killbillEvent) {
+
+ logService.log(LogService.LOG_INFO, "Received external event " + killbillEvent.toString());
+
+ // Only looking at account creation
+ if (killbillEvent.getEventType() != ExtBusEventType.ACCOUNT_CREATION) {
+ return;
+ }
+
+ final TenantContext tenantContext = new TenantContext() {
+ @Override
+ public UUID getTenantId() {
+ return null;
+ }
+ };
+
+ try {
+ Account account = killbillAPI.getAccountUserApi().getAccountById(killbillEvent.getAccountId(), tenantContext);
+ testDao.insertAccountExternalKey(account.getExternalKey());
+
+ } catch (AccountApiException e) {
+ logService.log(LogService.LOG_ERROR, e.getMessage());
+ }
+ }
+}
diff --git a/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java b/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java
new file mode 100644
index 0000000..21267b2
--- /dev/null
+++ b/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.test;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.osgi.bundles.test.dao.TestDao;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.plugin.api.RefundInfoPlugin;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+
+public class TestPaymentPluginApi implements PaymentPluginApi {
+
+ private final TestDao testDao;
+ private final String name;
+
+ public TestPaymentPluginApi(final String name, final TestDao testDao) {
+ this.testDao = testDao;
+ this.name = name;
+ }
+
+ @Override
+ public PaymentInfoPlugin processPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+ testDao.insertProcessedPayment(kbPaymentId, kbPaymentMethodId, amount);
+ return new PaymentInfoPlugin() {
+ @Override
+ public UUID getKbPaymentId() {
+ return kbPaymentId;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return new DateTime();
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return new DateTime();
+ }
+
+ @Override
+ public PaymentPluginStatus getStatus() {
+ return PaymentPluginStatus.PROCESSED;
+ }
+
+ @Override
+ public String getGatewayError() {
+ return null;
+ }
+
+ @Override
+ public String getGatewayErrorCode() {
+ return null;
+ }
+
+ @Override
+ public String getFirstPaymentReferenceId() {
+ return null;
+ }
+
+ @Override
+ public String getSecondPaymentReferenceId() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public PaymentInfoPlugin getPaymentInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) throws PaymentPluginApiException {
+ return null;
+ }
+
+ @Override
+ public Pagination<PaymentInfoPlugin> searchPayments(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return new Pagination<PaymentInfoPlugin>() {
+ @Override
+ public Long getCurrentOffset() {
+ return 0L;
+ }
+
+ @Override
+ public Long getNextOffset() {
+ return null;
+ }
+
+ @Override
+ public Long getMaxNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Long getTotalNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Iterator<PaymentInfoPlugin> iterator() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public RefundInfoPlugin processRefund(final UUID kbAccountId, final UUID kbPaymentId, final BigDecimal refundAmount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+ return null;
+ }
+
+ @Override
+ public List<RefundInfoPlugin> getRefundInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) {
+ return Collections.<RefundInfoPlugin>emptyList();
+ }
+
+ @Override
+ public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return new Pagination<RefundInfoPlugin>() {
+ @Override
+ public Long getCurrentOffset() {
+ return 0L;
+ }
+
+ @Override
+ public Long getNextOffset() {
+ return null;
+ }
+
+ @Override
+ public Long getMaxNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Long getTotalNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Iterator<RefundInfoPlugin> iterator() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public void deletePaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public PaymentMethodPlugin getPaymentMethodDetail(final UUID kbAccountId, final UUID kbPaymentMethodId, final TenantContext context) throws PaymentPluginApiException {
+ return null;
+ }
+
+ @Override
+ public void setDefaultPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public List<PaymentMethodInfoPlugin> getPaymentMethods(final UUID kbAccountId, final boolean refreshFromGateway, final CallContext context) throws PaymentPluginApiException {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return new Pagination<PaymentMethodPlugin>() {
+ @Override
+ public Long getCurrentOffset() {
+ return 0L;
+ }
+
+ @Override
+ public Long getNextOffset() {
+ return null;
+ }
+
+ @Override
+ public Long getMaxNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Long getTotalNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Iterator<PaymentMethodPlugin> iterator() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public void resetPaymentMethods(final UUID kbAccountId, final List<PaymentMethodInfoPlugin> paymentMethods) throws PaymentPluginApiException {
+ }
+}
osgi-bundles/tests/payment/pom.xml 22(+11 -11)
diff --git a/osgi-bundles/tests/payment/pom.xml b/osgi-bundles/tests/payment/pom.xml
index 5c49bac..2053ff1 100644
--- a/osgi-bundles/tests/payment/pom.xml
+++ b/osgi-bundles/tests/payment/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-test-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-bundles-test-payment</artifactId>
@@ -27,25 +27,25 @@
<name>Killbill billing platform: OSGI Beatrix Test payment</name>
<dependencies>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-osgi-bundles-lib-killbill</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<!-- Override the scope since we build this test as 'source' -->
<scope>compile</scope>
</dependency>
<dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
@@ -84,10 +84,10 @@
</executions>
<configuration>
<instructions>
- <Bundle-Activator>com.ning.billing.osgi.bundles.test.PaymentActivator</Bundle-Activator>
+ <Bundle-Activator>org.killbill.billing.osgi.bundles.test.PaymentActivator</Bundle-Activator>
<Import-Package>
<!-- maven-bundle-plugin does not seem to detect that the library is using OSGIKillbill, this is annoying... -->
- *;resolution:=optional,com.ning.billing.osgi.api
+ *;resolution:=optional,org.killbill.billing.osgi.api
</Import-Package>
</instructions>
</configuration>
diff --git a/osgi-bundles/tests/payment/src/main/java/org/killbill/billing/osgi/bundles/test/Dummy.java b/osgi-bundles/tests/payment/src/main/java/org/killbill/billing/osgi/bundles/test/Dummy.java
new file mode 100644
index 0000000..255e48e
--- /dev/null
+++ b/osgi-bundles/tests/payment/src/main/java/org/killbill/billing/osgi/bundles/test/Dummy.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.test;
+
+public class Dummy {
+
+}
diff --git a/osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/PaymentActivator.java b/osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/PaymentActivator.java
new file mode 100644
index 0000000..4225f31
--- /dev/null
+++ b/osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/PaymentActivator.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.test;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.osgi.framework.BundleContext;
+
+import org.killbill.billing.osgi.api.OSGIPluginProperties;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiWithTestControl;
+import org.killbill.killbill.osgi.libs.killbill.KillbillActivatorBase;
+import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillEventDispatcher.OSGIKillbillEventHandler;
+
+/**
+ * Test class used by Payment tests-- to test fake OSGI payment bundle
+ */
+public class PaymentActivator extends KillbillActivatorBase {
+
+ @Override
+ public void start(final BundleContext context) throws Exception {
+
+ final String bundleName = context.getBundle().getSymbolicName();
+ System.out.println("PaymentActivator starting bundle = " + bundleName);
+
+ super.start(context);
+ registerPaymentApi(context);
+ }
+
+ @Override
+ public void stop(final BundleContext context) throws Exception {
+ super.stop(context);
+ System.out.println("Good bye world from PaymentActivator!");
+ }
+
+ @Override
+ public OSGIKillbillEventHandler getOSGIKillbillEventHandler() {
+ return null;
+ }
+
+ private void registerPaymentApi(final BundleContext context) {
+
+ final Dictionary props = new Hashtable();
+ // Same name the beatrix tests expect when using that payment plugin
+ props.put(OSGIPluginProperties.PLUGIN_NAME_PROP, "osgi-payment-plugin");
+ registrar.registerService(context, PaymentPluginApiWithTestControl.class, new TestPaymentPluginApi("test"), props);
+ }
+}
diff --git a/osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java b/osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java
new file mode 100644
index 0000000..9c713ba
--- /dev/null
+++ b/osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.osgi.bundles.test;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiWithTestControl;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.plugin.api.RefundInfoPlugin;
+import org.killbill.billing.payment.plugin.api.RefundPluginStatus;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+
+public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
+
+ private final String name;
+
+ private PaymentPluginApiException paymentPluginApiExceptionOnNextCalls;
+ private RuntimeException runtimeExceptionOnNextCalls;
+
+ public TestPaymentPluginApi(final String name) {
+ this.name = name;
+ resetToNormalbehavior();
+ }
+
+ @Override
+ public PaymentInfoPlugin processPayment(final UUID accountId, final UUID kbPaymentId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+ return withRuntimeCheckForExceptions(new PaymentInfoPlugin() {
+ @Override
+ public UUID getKbPaymentId() {
+ return kbPaymentId;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return new DateTime();
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return new DateTime();
+ }
+
+ @Override
+ public PaymentPluginStatus getStatus() {
+ return PaymentPluginStatus.PROCESSED;
+ }
+
+ @Override
+ public String getGatewayError() {
+ return null;
+ }
+
+ @Override
+ public String getGatewayErrorCode() {
+ return null;
+ }
+
+ @Override
+ public String getFirstPaymentReferenceId() {
+ return null;
+ }
+
+ @Override
+ public String getSecondPaymentReferenceId() {
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public PaymentInfoPlugin getPaymentInfo(final UUID accountId, final UUID kbPaymentId, final TenantContext context) throws PaymentPluginApiException {
+
+ final BigDecimal someAmount = new BigDecimal("12.45");
+ return withRuntimeCheckForExceptions(new PaymentInfoPlugin() {
+ @Override
+ public UUID getKbPaymentId() {
+ return kbPaymentId;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return someAmount;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return null;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return new DateTime();
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return new DateTime();
+ }
+
+ @Override
+ public PaymentPluginStatus getStatus() {
+ return PaymentPluginStatus.PROCESSED;
+ }
+
+ @Override
+ public String getGatewayError() {
+ return null;
+ }
+
+ @Override
+ public String getGatewayErrorCode() {
+ return null;
+ }
+
+ @Override
+ public String getFirstPaymentReferenceId() {
+ return null;
+ }
+
+ @Override
+ public String getSecondPaymentReferenceId() {
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public Pagination<PaymentInfoPlugin> searchPayments(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return new Pagination<PaymentInfoPlugin>() {
+ @Override
+ public Long getCurrentOffset() {
+ return 0L;
+ }
+
+ @Override
+ public Long getNextOffset() {
+ return null;
+ }
+
+ @Override
+ public Long getMaxNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Long getTotalNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Iterator<PaymentInfoPlugin> iterator() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public RefundInfoPlugin processRefund(final UUID accountId, final UUID kbPaymentId, final BigDecimal refundAmount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+ return withRuntimeCheckForExceptions(new RefundInfoPlugin() {
+ @Override
+ public UUID getKbPaymentId() {
+ return kbPaymentId;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return null;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return null;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return null;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return null;
+ }
+
+ @Override
+ public RefundPluginStatus getStatus() {
+ return null;
+ }
+
+ @Override
+ public String getGatewayError() {
+ return null;
+ }
+
+ @Override
+ public String getGatewayErrorCode() {
+ return null;
+ }
+
+ @Override
+ public String getFirstRefundReferenceId() {
+ return null;
+ }
+
+ @Override
+ public String getSecondRefundReferenceId() {
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public List<RefundInfoPlugin> getRefundInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) {
+ return Collections.<RefundInfoPlugin>emptyList();
+ }
+
+ @Override
+ public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return new Pagination<RefundInfoPlugin>() {
+ @Override
+ public Long getCurrentOffset() {
+ return 0L;
+ }
+
+ @Override
+ public Long getNextOffset() {
+ return null;
+ }
+
+ @Override
+ public Long getMaxNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Long getTotalNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Iterator<RefundInfoPlugin> iterator() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public void deletePaymentMethod(final UUID accountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public PaymentMethodPlugin getPaymentMethodDetail(final UUID kbAccountId, final UUID kbPaymentMethodId, final TenantContext context) throws PaymentPluginApiException {
+ return null;
+ }
+
+ @Override
+ public void setDefaultPaymentMethod(final UUID accountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public List<PaymentMethodInfoPlugin> getPaymentMethods(final UUID kbAccountId, final boolean refreshFromGateway, final CallContext context) throws PaymentPluginApiException {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return new Pagination<PaymentMethodPlugin>() {
+ @Override
+ public Long getCurrentOffset() {
+ return 0L;
+ }
+
+ @Override
+ public Long getNextOffset() {
+ return null;
+ }
+
+ @Override
+ public Long getMaxNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Long getTotalNbRecords() {
+ return 0L;
+ }
+
+ @Override
+ public Iterator<PaymentMethodPlugin> iterator() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public void resetPaymentMethods(final UUID accountId, final List<PaymentMethodInfoPlugin> paymentMethods) throws PaymentPluginApiException {
+ }
+
+ private <T> T withRuntimeCheckForExceptions(final T result) throws PaymentPluginApiException {
+ if (paymentPluginApiExceptionOnNextCalls != null) {
+ throw paymentPluginApiExceptionOnNextCalls;
+
+ } else if (runtimeExceptionOnNextCalls != null) {
+ throw runtimeExceptionOnNextCalls;
+ } else {
+ return result;
+ }
+ }
+
+ @Override
+ public void setPaymentPluginApiExceptionOnNextCalls(final PaymentPluginApiException e) {
+ resetToNormalbehavior();
+ paymentPluginApiExceptionOnNextCalls = e;
+ }
+
+ @Override
+ public void setPaymentRuntimeExceptionOnNextCalls(final RuntimeException e) {
+ resetToNormalbehavior();
+ runtimeExceptionOnNextCalls = e;
+ }
+
+ @Override
+ public void resetToNormalbehavior() {
+ paymentPluginApiExceptionOnNextCalls = null;
+ runtimeExceptionOnNextCalls = null;
+ }
+}
osgi-bundles/tests/pom.xml 4(+2 -2)
diff --git a/osgi-bundles/tests/pom.xml b/osgi-bundles/tests/pom.xml
index b291a1d..18750a0 100644
--- a/osgi-bundles/tests/pom.xml
+++ b/osgi-bundles/tests/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-osgi-all-bundles</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-osgi-test-bundles</artifactId>
overdue/pom.xml 72(+26 -46)
diff --git a/overdue/pom.xml b/overdue/pom.xml
index 2242c44..7399bc1 100644
--- a/overdue/pom.xml
+++ b/overdue/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-overdue</artifactId>
@@ -41,93 +41,73 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-java</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
@@ -168,7 +148,7 @@
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
- <Main-Class>com.ning.billing.overdue.CreateOverdueConfigSchema</Main-Class>
+ <Main-Class>org.killbill.billing.overdue.CreateOverdueConfigSchema</Main-Class>
</manifestEntries>
</transformer>
</transformers>
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueUserApi.java b/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueUserApi.java
new file mode 100644
index 0000000..af236d0
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueUserApi.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.api;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.overdue.OverdueApiException;
+import org.killbill.billing.overdue.OverdueService;
+import org.killbill.billing.overdue.OverdueState;
+import org.killbill.billing.overdue.OverdueUserApi;
+import org.killbill.billing.overdue.config.OverdueConfig;
+import org.killbill.billing.overdue.config.api.BillingState;
+import org.killbill.billing.overdue.config.api.OverdueException;
+import org.killbill.billing.overdue.config.api.OverdueStateSet;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
+import org.killbill.billing.overdue.wrapper.OverdueWrapperFactory;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+import com.google.inject.Inject;
+
+public class DefaultOverdueUserApi implements OverdueUserApi {
+
+ Logger log = LoggerFactory.getLogger(DefaultOverdueUserApi.class);
+
+ private final OverdueWrapperFactory factory;
+ private final BlockingInternalApi accessApi;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ private OverdueConfig overdueConfig;
+
+ @Inject
+ public DefaultOverdueUserApi(final OverdueWrapperFactory factory, final BlockingInternalApi accessApi, final InternalCallContextFactory internalCallContextFactory) {
+ this.factory = factory;
+ this.accessApi = accessApi;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public OverdueState getOverdueStateFor(final Account overdueable, final TenantContext context) throws OverdueException {
+ try {
+ final String stateName = accessApi.getBlockingStateForService(overdueable.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContextFactory.createInternalTenantContext(context)).getStateName();
+ final OverdueStateSet states = overdueConfig.getStateSet();
+ return states.findState(stateName);
+ } catch (OverdueApiException e) {
+ throw new OverdueException(e, ErrorCode.OVERDUE_CAT_ERROR_ENCOUNTERED, overdueable.getId(), overdueable.getClass().getSimpleName());
+ }
+ }
+
+ @Override
+ public BillingState getBillingStateFor(final Account overdueable, final TenantContext context) throws OverdueException {
+ log.debug("Billing state of of {} requested", overdueable.getId());
+ final OverdueWrapper wrapper = factory.createOverdueWrapperFor(overdueable);
+ return wrapper.billingState(internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public OverdueState refreshOverdueStateFor(final Account blockable, final CallContext context) throws OverdueException, OverdueApiException {
+ log.info("Refresh of blockable {} ({}) requested", blockable.getId(), blockable.getClass());
+ final OverdueWrapper wrapper = factory.createOverdueWrapperFor(blockable);
+ return wrapper.refresh(createInternalCallContext(blockable, context));
+ }
+
+ private InternalCallContext createInternalCallContext(final Account blockable, final CallContext context) {
+ return internalCallContextFactory.createInternalCallContext(blockable.getId(), ObjectType.ACCOUNT, context);
+ }
+
+ @Override
+ public void setOverrideBillingStateForAccount(final Account overdueable, final BillingState state, final CallContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setOverdueConfig(final OverdueConfig config) {
+ this.overdueConfig = config;
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/applicator/DefaultOverdueChangeEvent.java b/overdue/src/main/java/org/killbill/billing/overdue/applicator/DefaultOverdueChangeEvent.java
new file mode 100644
index 0000000..0bf53a3
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/DefaultOverdueChangeEvent.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.applicator;
+
+import java.util.UUID;
+
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.OverdueChangeInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultOverdueChangeEvent extends BusEventBase implements OverdueChangeInternalEvent {
+
+ private final UUID overdueObjectId;
+ private final String previousOverdueStateName;
+ private final String nextOverdueStateName;
+ private final Boolean isBlockedBilling;
+ private final Boolean isUnblockedBilling;
+
+
+ @JsonCreator
+ public DefaultOverdueChangeEvent(@JsonProperty("overdueObjectId") final UUID overdueObjectId,
+ @JsonProperty("previousOverdueStateName") final String previousOverdueStateName,
+ @JsonProperty("nextOverdueStateName") final String nextOverdueStateName,
+ @JsonProperty("isBlockedBilling") final Boolean isBlockedBilling,
+ @JsonProperty("isUnblockedBilling") final Boolean isUnblockedBilling,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.overdueObjectId = overdueObjectId;
+ this.isBlockedBilling = isBlockedBilling;
+ this.isUnblockedBilling = isUnblockedBilling;
+ this.previousOverdueStateName = previousOverdueStateName;
+ this.nextOverdueStateName = nextOverdueStateName;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.OVERDUE_CHANGE;
+ }
+
+ @Override
+ public String getPreviousOverdueStateName() {
+ return previousOverdueStateName;
+ }
+
+ @Override
+ public UUID getOverdueObjectId() {
+ return overdueObjectId;
+ }
+
+ @Override
+ public String getNextOverdueStateName() {
+ return nextOverdueStateName;
+ }
+
+ @Override
+ @JsonProperty("isBlockedBilling")
+ public Boolean isBlockedBilling() {
+ return isBlockedBilling;
+ }
+
+ @Override
+ @JsonProperty("isUnblockedBilling")
+ public Boolean isUnblockedBilling() {
+ return isUnblockedBilling;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultOverdueChangeEvent{");
+ sb.append("overdueObjectId=").append(overdueObjectId);
+ sb.append(", previousOverdueStateName='").append(previousOverdueStateName).append('\'');
+ sb.append(", nextOverdueStateName='").append(nextOverdueStateName).append('\'');
+ sb.append(", isBlockedBilling=").append(isBlockedBilling);
+ sb.append(", isUnblockedBilling=").append(isUnblockedBilling);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DefaultOverdueChangeEvent)) {
+ return false;
+ }
+
+ final DefaultOverdueChangeEvent that = (DefaultOverdueChangeEvent) o;
+
+ if (isBlockedBilling != null ? !isBlockedBilling.equals(that.isBlockedBilling) : that.isBlockedBilling != null) {
+ return false;
+ }
+ if (isUnblockedBilling != null ? !isUnblockedBilling.equals(that.isUnblockedBilling) : that.isUnblockedBilling != null) {
+ return false;
+ }
+ if (nextOverdueStateName != null ? !nextOverdueStateName.equals(that.nextOverdueStateName) : that.nextOverdueStateName != null) {
+ return false;
+ }
+ if (overdueObjectId != null ? !overdueObjectId.equals(that.overdueObjectId) : that.overdueObjectId != null) {
+ return false;
+ }
+ if (previousOverdueStateName != null ? !previousOverdueStateName.equals(that.previousOverdueStateName) : that.previousOverdueStateName != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = overdueObjectId != null ? overdueObjectId.hashCode() : 0;
+ result = 31 * result + (previousOverdueStateName != null ? previousOverdueStateName.hashCode() : 0);
+ result = 31 * result + (nextOverdueStateName != null ? nextOverdueStateName.hashCode() : 0);
+ result = 31 * result + (isBlockedBilling != null ? isBlockedBilling.hashCode() : 0);
+ result = 31 * result + (isUnblockedBilling != null ? isUnblockedBilling.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/applicator/formatters/DefaultBillingStateFormatter.java b/overdue/src/main/java/org/killbill/billing/overdue/applicator/formatters/DefaultBillingStateFormatter.java
new file mode 100644
index 0000000..a71170e
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/formatters/DefaultBillingStateFormatter.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.applicator.formatters;
+
+import java.math.BigDecimal;
+
+import org.killbill.billing.overdue.config.api.BillingState;
+
+import com.google.common.base.Objects;
+
+import static org.killbill.billing.util.DefaultAmountFormatter.round;
+
+public class DefaultBillingStateFormatter extends BillingStateFormatter {
+
+ public DefaultBillingStateFormatter(final BillingState billingState) {
+ super(billingState);
+ }
+
+ @Override
+ public String getFormattedBalanceOfUnpaidInvoices() {
+ return round(Objects.firstNonNull(getBalanceOfUnpaidInvoices(), BigDecimal.ZERO)).toString();
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/applicator/formatters/DefaultOverdueEmailFormatterFactory.java b/overdue/src/main/java/org/killbill/billing/overdue/applicator/formatters/DefaultOverdueEmailFormatterFactory.java
new file mode 100644
index 0000000..3f5b7a3
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/formatters/DefaultOverdueEmailFormatterFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.applicator.formatters;
+
+import org.killbill.billing.overdue.config.api.BillingState;
+
+public class DefaultOverdueEmailFormatterFactory implements OverdueEmailFormatterFactory {
+
+ @Override
+ public BillingStateFormatter createBillingStateFormatter(final BillingState billingState) {
+ return new DefaultBillingStateFormatter(billingState);
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueEmailGenerator.java b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueEmailGenerator.java
new file mode 100644
index 0000000..bfbc990
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueEmailGenerator.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.applicator;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.entitlement.api.Blockable;
+import org.killbill.billing.overdue.OverdueState;
+import org.killbill.billing.overdue.applicator.formatters.OverdueEmailFormatterFactory;
+import org.killbill.billing.overdue.config.api.BillingState;
+import org.killbill.billing.util.email.templates.TemplateEngine;
+
+import com.google.inject.Inject;
+
+public class OverdueEmailGenerator {
+
+ private final TemplateEngine templateEngine;
+ private final OverdueEmailFormatterFactory overdueEmailFormatterFactory;
+
+ @Inject
+ public OverdueEmailGenerator(final TemplateEngine templateEngine, final OverdueEmailFormatterFactory overdueEmailFormatterFactory) {
+ this.templateEngine = templateEngine;
+ this.overdueEmailFormatterFactory = overdueEmailFormatterFactory;
+ }
+
+ public String generateEmail(final Account account, final BillingState billingState,
+ final Account overdueable, final OverdueState nextOverdueState) throws IOException {
+ final Map<String, Object> data = new HashMap<String, Object>();
+
+ // TODO raw objects for now. We eventually should respect the account locale and support translations
+ data.put("account", account);
+ data.put("billingState", overdueEmailFormatterFactory.createBillingStateFormatter(billingState));
+ data.put("overdueable", overdueable);
+ data.put("nextOverdueState", nextOverdueState);
+
+ // TODO single template for all languages for now
+ return templateEngine.executeTemplate(nextOverdueState.getEnterStateEmailNotification().getTemplateName(), data);
+ }
+}
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
new file mode 100644
index 0000000..1e8b674
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.applicator;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Named;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.joda.time.Period;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.bus.api.PersistentBus;
+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.clock.Clock;
+import org.killbill.billing.entitlement.api.BlockingApiException;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.EntitlementApi;
+import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.events.OverdueChangeInternalEvent;
+import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.junction.DefaultBlockingState;
+import org.killbill.billing.overdue.OverdueApiException;
+import org.killbill.billing.overdue.OverdueCancellationPolicy;
+import org.killbill.billing.overdue.OverdueService;
+import org.killbill.billing.overdue.OverdueState;
+import org.killbill.billing.overdue.config.api.BillingState;
+import org.killbill.billing.overdue.config.api.OverdueException;
+import org.killbill.billing.overdue.config.api.OverdueStateSet;
+import org.killbill.billing.overdue.glue.DefaultOverdueModule;
+import org.killbill.billing.overdue.notification.OverdueCheckNotificationKey;
+import org.killbill.billing.overdue.notification.OverdueCheckNotifier;
+import org.killbill.billing.overdue.notification.OverduePoster;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.dao.NonEntityDao;
+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 com.google.common.base.Predicate;
+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 {
+
+ private static final Logger log = LoggerFactory.getLogger(OverdueStateApplicator.class);
+
+ private final BlockingInternalApi blockingApi;
+ private final Clock clock;
+ private final OverduePoster checkPoster;
+ private final PersistentBus bus;
+ private final AccountInternalApi accountApi;
+ private final EntitlementApi entitlementApi;
+ private final OverdueEmailGenerator overdueEmailGenerator;
+ private final TagInternalApi tagApi;
+ private final EmailSender emailSender;
+ private final NonEntityDao nonEntityDao;
+
+ @Inject
+ public OverdueStateApplicator(final BlockingInternalApi accessApi,
+ final AccountInternalApi accountApi,
+ final EntitlementApi entitlementApi,
+ final Clock clock,
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_CHECK_NAMED) final OverduePoster checkPoster,
+ final OverdueEmailGenerator overdueEmailGenerator,
+ final EmailConfig config,
+ final PersistentBus bus,
+ final NonEntityDao nonEntityDao,
+ final TagInternalApi tagApi) {
+
+ this.blockingApi = accessApi;
+ this.accountApi = accountApi;
+ this.entitlementApi = entitlementApi;
+ this.clock = clock;
+ this.checkPoster = checkPoster;
+ this.overdueEmailGenerator = overdueEmailGenerator;
+ this.tagApi = tagApi;
+ this.nonEntityDao = nonEntityDao;
+ this.emailSender = new DefaultEmailSender(config);
+ this.bus = bus;
+ }
+
+ public void apply(final OverdueStateSet overdueStateSet, final BillingState billingState,
+ final Account account, final OverdueState previousOverdueState,
+ final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueException {
+ try {
+
+ if (isAccountTaggedWith_OVERDUE_ENFORCEMENT_OFF(context)) {
+ log.debug("OverdueStateApplicator:apply returns because account (recordId = " + context.getAccountRecordId() + ") is set with OVERDUE_ENFORCEMENT_OFF ");
+ return;
+ }
+
+ log.debug("OverdueStateApplicator:apply <enter> : time = " + clock.getUTCNow() + ", previousState = " + previousOverdueState.getName() + ", nextState = " + nextOverdueState);
+
+ 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 = nextOverdueState.isClearState() ? overdueStateSet.getInitialReevaluationInterval() : nextOverdueState.getReevaluationInterval();
+ // 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 " + account.getId() + ", time = " + clock.getUTCNow().plus(reevaluationInterval));
+ createFutureNotification(account, clock.getUTCNow().plus(reevaluationInterval), context);
+ }
+
+ } else if (nextOverdueState.isClearState()) {
+ clearFutureNotification(account, context);
+ }
+
+ if (previousOverdueState.getName().equals(nextOverdueState.getName())) {
+ return;
+ }
+
+ cancelSubscriptionsIfRequired(account, nextOverdueState, context);
+
+ sendEmailIfRequired(billingState, account, 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(account, nextOverdueState, context);
+ } catch (OverdueApiException e) {
+ if (e.getCode() != ErrorCode.OVERDUE_NO_REEVALUATION_INTERVAL.getCode()) {
+ throw new OverdueException(e);
+ }
+ }
+ try {
+ bus.post(createOverdueEvent(account, previousOverdueState.getName(), nextOverdueState.getName(), isBlockBillingTransition(previousOverdueState, nextOverdueState),
+ isUnblockBillingTransition(previousOverdueState, nextOverdueState), context));
+ } catch (Exception e) {
+ log.error("Error posting overdue change event to bus", e);
+ }
+ }
+
+ private void avoid_extra_credit_by_toggling_AUTO_INVOICE_OFF(final Account account, final OverdueState previousOverdueState,
+ final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueApiException {
+ if (isBlockBillingTransition(previousOverdueState, nextOverdueState)) {
+ set_AUTO_INVOICE_OFF_on_blockedBilling(account.getId(), context);
+ } else if (isUnblockBillingTransition(previousOverdueState, nextOverdueState)) {
+ remove_AUTO_INVOICE_OFF_on_clear(account.getId(), context);
+ }
+ }
+
+ public void clear(final Account account, final OverdueState previousOverdueState, final OverdueState clearState, final InternalCallContext context) throws OverdueException {
+
+ log.debug("OverdueStateApplicator:clear : time = " + clock.getUTCNow() + ", previousState = " + previousOverdueState.getName());
+
+ storeNewState(account, clearState, context);
+
+ clearFutureNotification(account, context);
+
+ try {
+ avoid_extra_credit_by_toggling_AUTO_INVOICE_OFF(account, previousOverdueState, clearState, context);
+ } catch (OverdueApiException e) {
+ throw new OverdueException(e);
+ }
+
+ try {
+ bus.post(createOverdueEvent(account, previousOverdueState.getName(), clearState.getName(), isBlockBillingTransition(previousOverdueState, clearState),
+ isUnblockBillingTransition(previousOverdueState, clearState), context));
+ } catch (Exception e) {
+ log.error("Error posting overdue change event to bus", e);
+ }
+ }
+
+ private OverdueChangeInternalEvent createOverdueEvent(final Account overdueable, final String previousOverdueStateName, final String nextOverdueStateName,
+ final boolean isBlockedBilling, final boolean isUnblockedBilling, final InternalCallContext context) throws BlockingApiException {
+ return new DefaultOverdueChangeEvent(overdueable.getId(), previousOverdueStateName, nextOverdueStateName, isBlockedBilling, isUnblockedBilling,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+ }
+
+ protected void storeNewState(final Account blockable, final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueException {
+ try {
+ blockingApi.setBlockingState(new DefaultBlockingState(blockable.getId(),
+ BlockingStateType.ACCOUNT,
+ nextOverdueState.getName(),
+ OverdueService.OVERDUE_SERVICE_NAME,
+ blockChanges(nextOverdueState),
+ blockEntitlement(nextOverdueState),
+ blockBilling(nextOverdueState),
+ clock.getUTCNow()),
+ context);
+ } catch (Exception e) {
+ throw new OverdueException(e, ErrorCode.OVERDUE_CAT_ERROR_ENCOUNTERED, blockable.getId(), blockable.getClass().getName());
+ }
+ }
+
+ private void set_AUTO_INVOICE_OFF_on_blockedBilling(final UUID accountId, final InternalCallContext context) throws OverdueApiException {
+ try {
+ tagApi.addTag(accountId, ObjectType.ACCOUNT, ControlTagType.AUTO_INVOICING_OFF.getId(), context);
+ } catch (TagApiException e) {
+ throw new OverdueApiException(e);
+ }
+ }
+
+ private void remove_AUTO_INVOICE_OFF_on_clear(final UUID accountId, final InternalCallContext context) throws OverdueApiException {
+ try {
+ tagApi.removeTag(accountId, ObjectType.ACCOUNT, ControlTagType.AUTO_INVOICING_OFF.getId(), context);
+ } catch (TagApiException e) {
+ if (e.getCode() != ErrorCode.TAG_DOES_NOT_EXIST.getCode()) {
+ throw new OverdueApiException(e);
+ }
+ }
+ }
+
+ private boolean isBlockBillingTransition(final OverdueState prevOverdueState, final OverdueState nextOverdueState) {
+ return !blockBilling(prevOverdueState) && blockBilling(nextOverdueState);
+ }
+
+ private boolean isUnblockBillingTransition(final OverdueState prevOverdueState, final OverdueState nextOverdueState) {
+ return blockBilling(prevOverdueState) && !blockBilling(nextOverdueState);
+ }
+
+ private boolean blockChanges(final OverdueState nextOverdueState) {
+ return nextOverdueState.blockChanges();
+ }
+
+ private boolean blockBilling(final OverdueState nextOverdueState) {
+ return nextOverdueState.disableEntitlementAndChangesBlocked();
+ }
+
+ private boolean blockEntitlement(final OverdueState nextOverdueState) {
+ return nextOverdueState.disableEntitlementAndChangesBlocked();
+ }
+
+ protected void createFutureNotification(final Account account, final DateTime timeOfNextCheck, final InternalCallContext context) {
+ final OverdueCheckNotificationKey notificationKey = new OverdueCheckNotificationKey(account.getId());
+ checkPoster.insertOverdueNotification(account.getId(), timeOfNextCheck, OverdueCheckNotifier.OVERDUE_CHECK_NOTIFIER_QUEUE, notificationKey, context);
+ }
+
+ protected void clearFutureNotification(final Account account, final InternalCallContext context) {
+ // Need to clear the override table here too (when we add it)
+ checkPoster.clearOverdueCheckNotifications(account.getId(), OverdueCheckNotifier.OVERDUE_CHECK_NOTIFIER_QUEUE, OverdueCheckNotificationKey.class, context);
+ }
+
+ private void cancelSubscriptionsIfRequired(final Account account, final OverdueState nextOverdueState, final InternalCallContext context) throws OverdueException {
+ if (nextOverdueState.getSubscriptionCancellationPolicy() == OverdueCancellationPolicy.NONE) {
+ return;
+ }
+ try {
+ final BillingActionPolicy actionPolicy;
+ switch (nextOverdueState.getSubscriptionCancellationPolicy()) {
+ case END_OF_TERM:
+ actionPolicy = BillingActionPolicy.END_OF_TERM;
+ break;
+ case IMMEDIATE:
+ actionPolicy = BillingActionPolicy.IMMEDIATE;
+ break;
+ default:
+ throw new IllegalStateException("Unexpected OverdueCancellationPolicy " + nextOverdueState.getSubscriptionCancellationPolicy());
+ }
+ final List<Entitlement> toBeCancelled = new LinkedList<Entitlement>();
+ computeEntitlementsToCancel(account, toBeCancelled, context);
+
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT);
+ for (final Entitlement cur : toBeCancelled) {
+ try {
+ cur.cancelEntitlementWithDateOverrideBillingPolicy(new LocalDate(clock.getUTCNow(), account.getTimeZone()), actionPolicy, context.toCallContext(tenantId));
+ } catch (EntitlementApiException e) {
+ // If subscription has already been cancelled, there is nothing to do so we can ignore
+ if (e.getCode() != ErrorCode.SUB_CANCEL_BAD_STATE.getCode()) {
+ throw new OverdueException(e);
+ }
+ }
+ }
+ } catch (EntitlementApiException e) {
+ throw new OverdueException(e);
+ }
+ }
+
+ private void computeEntitlementsToCancel(final Account account, final List<Entitlement> result, final InternalTenantContext context) throws EntitlementApiException {
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT);
+ final List<Entitlement> allEntitlementsForAccountId = entitlementApi.getAllEntitlementsForAccountId(account.getId(), context.toTenantContext(tenantId));
+ // Entitlement is smart enough and will cancel the associated add-ons. See also discussion in https://github.com/killbill/killbill/issues/94
+ final Collection<Entitlement> allEntitlementsButAddonsForAccountId = Collections2.<Entitlement>filter(allEntitlementsForAccountId,
+ new Predicate<Entitlement>() {
+ @Override
+ public boolean apply(final Entitlement entitlement) {
+ return !ProductCategory.ADD_ON.equals(entitlement.getLastActiveProductCategory());
+ }
+ });
+ result.addAll(allEntitlementsButAddonsForAccountId);
+ }
+
+ private void sendEmailIfRequired(final BillingState billingState, final Account account,
+ final OverdueState nextOverdueState, final InternalTenantContext context) {
+ // 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.getEnterStateEmailNotification() == null) {
+ 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.getEnterStateEmailNotification().getSubject();
+
+ try {
+ // Generate and send the email
+ final String emailBody = overdueEmailGenerator.generateEmail(account, billingState, account, nextOverdueState);
+ if (nextOverdueState.getEnterStateEmailNotification().isHTML()) {
+ emailSender.sendHTMLEmail(to, cc, subject, emailBody);
+ } else {
+ emailSender.sendPlainTextEmail(to, cc, subject, emailBody);
+ }
+ } catch (IOException e) {
+ log.warn(String.format("Unable to generate or send overdue notification email for account %s and overdueable %s", account.getId(), account.getId()), e);
+ } catch (EmailApiException e) {
+ log.warn(String.format("Unable to send overdue notification email for account %s and overdueable %s", account.getId(), account.getId()), e);
+ } catch (MustacheException e) {
+ log.warn(String.format("Unable to generate overdue notification email for account %s and overdueable %s", account.getId(), account.getId()), e);
+ }
+ }
+
+ //
+ // Uses callcontext information to retrieve account matching the Overduable object and check whether we should do any overdue processing
+ //
+ private boolean isAccountTaggedWith_OVERDUE_ENFORCEMENT_OFF(final InternalCallContext context) throws OverdueException {
+
+ try {
+ final UUID accountId = accountApi.getByRecordId(context.getAccountRecordId(), context);
+
+ final List<Tag> accountTags = tagApi.getTags(accountId, ObjectType.ACCOUNT, context);
+ for (Tag cur : accountTags) {
+ if (cur.getTagDefinitionId().equals(ControlTagType.OVERDUE_ENFORCEMENT_OFF.getId())) {
+ return true;
+ }
+ }
+ return false;
+ } catch (AccountApiException e) {
+ throw new OverdueException(e);
+ }
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java b/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java
new file mode 100644
index 0000000..80ca3cc
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.calculator;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.NoSuchElementException;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.clock.Clock;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.overdue.config.api.BillingState;
+import org.killbill.billing.overdue.config.api.OverdueException;
+import org.killbill.billing.overdue.config.api.PaymentResponse;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.util.tag.Tag;
+
+import com.google.inject.Inject;
+
+public class BillingStateCalculator {
+
+ private final InvoiceInternalApi invoiceApi;
+ private final Clock clock;
+
+ protected class InvoiceDateComparator implements Comparator<Invoice> {
+
+ @Override
+ public int compare(final Invoice i1, final Invoice i2) {
+ final LocalDate d1 = i1.getInvoiceDate();
+ final LocalDate d2 = i2.getInvoiceDate();
+ if (d1.compareTo(d2) == 0) {
+ return i1.hashCode() - i2.hashCode(); // consistent (arbitrary) resolution for tied dates
+ }
+ return d1.compareTo(d2);
+ }
+ }
+
+ @Inject
+ public BillingStateCalculator(final InvoiceInternalApi invoiceApi, final Clock clock) {
+ this.invoiceApi = invoiceApi;
+ this.clock = clock;
+ }
+
+ public BillingState calculateBillingState(final Account account, final InternalTenantContext context) throws OverdueException {
+ final SortedSet<Invoice> unpaidInvoices = unpaidInvoicesForAccount(account.getId(), account.getTimeZone(), context);
+
+ final int numberOfUnpaidInvoices = unpaidInvoices.size();
+ final BigDecimal unpaidInvoiceBalance = sumBalance(unpaidInvoices);
+ LocalDate dateOfEarliestUnpaidInvoice = null;
+ UUID idOfEarliestUnpaidInvoice = null;
+ final Invoice invoice = earliest(unpaidInvoices);
+ if (invoice != null) {
+ dateOfEarliestUnpaidInvoice = invoice.getInvoiceDate();
+ idOfEarliestUnpaidInvoice = invoice.getId();
+ }
+ final PaymentResponse responseForLastFailedPayment = PaymentResponse.INSUFFICIENT_FUNDS; //TODO MDW
+ final Tag[] tags = new Tag[]{}; //TODO MDW
+
+
+ return new BillingState(account.getId(), numberOfUnpaidInvoices, unpaidInvoiceBalance, dateOfEarliestUnpaidInvoice, account.getTimeZone(), idOfEarliestUnpaidInvoice, responseForLastFailedPayment, tags);
+ }
+
+ // Package scope for testing
+ Invoice earliest(final SortedSet<Invoice> unpaidInvoices) {
+ try {
+ return unpaidInvoices.first();
+ } catch (NoSuchElementException e) {
+ return null;
+ }
+ }
+
+ BigDecimal sumBalance(final SortedSet<Invoice> unpaidInvoices) {
+ BigDecimal sum = BigDecimal.ZERO;
+ for (final Invoice unpaidInvoice : unpaidInvoices) {
+ sum = sum.add(unpaidInvoice.getBalance());
+ }
+ return sum;
+ }
+
+ SortedSet<Invoice> unpaidInvoicesForAccount(final UUID accountId, final DateTimeZone accountTimeZone, final InternalTenantContext context) {
+ final Collection<Invoice> invoices = invoiceApi.getUnpaidInvoicesByAccountId(accountId, clock.getToday(accountTimeZone), context);
+ final SortedSet<Invoice> sortedInvoices = new TreeSet<Invoice>(new InvoiceDateComparator());
+ sortedInvoices.addAll(invoices);
+ return sortedInvoices;
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultCondition.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultCondition.java
new file mode 100644
index 0000000..a4e6bb0
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultCondition.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+import java.math.BigDecimal;
+import java.net.URI;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Duration;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.billing.overdue.Condition;
+import org.killbill.billing.overdue.config.api.BillingState;
+import org.killbill.billing.overdue.config.api.PaymentResponse;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.Tag;
+
+@XmlAccessorType(XmlAccessType.NONE)
+
+public class DefaultCondition extends ValidatingConfig<OverdueConfig> implements Condition {
+
+ @XmlElement(required = false, name = "numberOfUnpaidInvoicesEqualsOrExceeds")
+ private Integer numberOfUnpaidInvoicesEqualsOrExceeds;
+
+ @XmlElement(required = false, name = "totalUnpaidInvoiceBalanceEqualsOrExceeds")
+ private BigDecimal totalUnpaidInvoiceBalanceEqualsOrExceeds;
+
+ @XmlElement(required = false, name = "timeSinceEarliestUnpaidInvoiceEqualsOrExceeds")
+ private DefaultDuration timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+
+ @XmlElementWrapper(required = false, name = "responseForLastFailedPaymentIn")
+ @XmlElement(required = false, name = "response")
+ private PaymentResponse[] responseForLastFailedPayment;
+
+ @XmlElement(required = false, name = "controlTag")
+ private ControlTagType controlTag;
+
+ @Override
+ public boolean evaluate(final BillingState state, final LocalDate date) {
+ LocalDate unpaidInvoiceTriggerDate = null;
+ if (timeSinceEarliestUnpaidInvoiceEqualsOrExceeds != null && state.getDateOfEarliestUnpaidInvoice() != null) { // no date => no unpaid invoices
+ unpaidInvoiceTriggerDate = state.getDateOfEarliestUnpaidInvoice().plus(timeSinceEarliestUnpaidInvoiceEqualsOrExceeds.toJodaPeriod());
+ }
+
+ return
+ (numberOfUnpaidInvoicesEqualsOrExceeds == null || state.getNumberOfUnpaidInvoices() >= numberOfUnpaidInvoicesEqualsOrExceeds) &&
+ (totalUnpaidInvoiceBalanceEqualsOrExceeds == null || totalUnpaidInvoiceBalanceEqualsOrExceeds.compareTo(state.getBalanceOfUnpaidInvoices()) <= 0) &&
+ (timeSinceEarliestUnpaidInvoiceEqualsOrExceeds == null ||
+ (unpaidInvoiceTriggerDate != null && !unpaidInvoiceTriggerDate.isAfter(date))) &&
+ (responseForLastFailedPayment == null || responseIsIn(state.getResponseForLastFailedPayment(), responseForLastFailedPayment)) &&
+ (controlTag == null || isTagIn(controlTag, state.getTags()));
+ }
+
+ private boolean responseIsIn(final PaymentResponse actualResponse,
+ final PaymentResponse[] responseForLastFailedPayment) {
+ for (final PaymentResponse response : responseForLastFailedPayment) {
+ if (response.equals(actualResponse)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isTagIn(final ControlTagType tagType, final Tag[] tags) {
+ for (final Tag t : tags) {
+ if (t.getTagDefinitionId().equals(tagType.getId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public ValidationErrors validate(final OverdueConfig root,
+ final ValidationErrors errors) {
+ return errors;
+ }
+
+ @Override
+ public void initialize(final OverdueConfig root, final URI uri) {
+ }
+
+ public Duration getTimeOffset() {
+ if (timeSinceEarliestUnpaidInvoiceEqualsOrExceeds != null) {
+ return timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+ } else {
+ return new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(0); // zero time
+ }
+
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java
new file mode 100644
index 0000000..9dbe657
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+
+import org.joda.time.DateTime;
+import org.joda.time.Period;
+
+import org.killbill.billing.catalog.api.Duration;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultDuration extends ValidatingConfig<OverdueConfig> implements Duration {
+ @XmlElement(required = true)
+ private TimeUnit unit;
+
+ @XmlElement(required = false)
+ private Integer number = -1;
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.IDuration#getUnit()
+ */
+ @Override
+ public TimeUnit getUnit() {
+ return unit;
+ }
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.IDuration#getLength()
+ */
+ @Override
+ public int getNumber() {
+ return number;
+ }
+
+ @Override
+ public DateTime addToDateTime(final DateTime dateTime) {
+ if ((number == null) && (unit != TimeUnit.UNLIMITED)) {
+ return dateTime;
+ }
+
+ switch (unit) {
+ case DAYS:
+ return dateTime.plusDays(number);
+ case MONTHS:
+ return dateTime.plusMonths(number);
+ case YEARS:
+ return dateTime.plusYears(number);
+ case UNLIMITED:
+ return dateTime.plusYears(100);
+ default:
+ return dateTime;
+ }
+ }
+
+ @Override
+ public Period toJodaPeriod() {
+ if ((number == null) && (unit != TimeUnit.UNLIMITED)) {
+ return new Period();
+ }
+
+ switch (unit) {
+ case DAYS:
+ return new Period().withDays(number);
+ case MONTHS:
+ return new Period().withMonths(number);
+ case YEARS:
+ return new Period().withYears(number);
+ case UNLIMITED:
+ return new Period().withYears(100);
+ default:
+ return new Period();
+ }
+ }
+
+ @Override
+ public ValidationErrors validate(final OverdueConfig catalog, final ValidationErrors errors) {
+ //TODO MDW - Validation TimeUnit UNLIMITED iff number == -1
+ return errors;
+ }
+
+ protected DefaultDuration setUnit(final TimeUnit unit) {
+ this.unit = unit;
+ return this;
+ }
+
+ protected DefaultDuration setNumber(final Integer number) {
+ this.number = number;
+ return this;
+ }
+
+
+}
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
new file mode 100644
index 0000000..054a694
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultEmailNotification.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+
+import org.killbill.billing.overdue.EmailNotification;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultEmailNotification implements EmailNotification {
+
+ @XmlElement(required = true, name = "subject")
+ private String subject;
+
+ @XmlElement(required = true, name = "templateName")
+ private String templateName;
+
+ @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
new file mode 100644
index 0000000..a4d1338
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlID;
+
+import org.joda.time.Period;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.billing.overdue.EmailNotification;
+import org.killbill.billing.overdue.OverdueApiException;
+import org.killbill.billing.overdue.OverdueCancellationPolicy;
+import org.killbill.billing.overdue.OverdueState;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationError;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultOverdueState extends ValidatingConfig<OverdueConfig> implements OverdueState {
+
+ private static final int MAX_NAME_LENGTH = 50;
+
+ @XmlElement(required = false, name = "condition")
+ private DefaultCondition condition;
+
+ @XmlAttribute(required = true, name = "name")
+ @XmlID
+ private String name;
+
+ @XmlElement(required = false, name = "externalMessage")
+ private String externalMessage = "";
+
+ @XmlElement(required = false, name = "blockChanges")
+ private Boolean blockChanges = false;
+
+ @XmlElement(required = false, name = "disableEntitlementAndChangesBlocked")
+ private Boolean disableEntitlement = false;
+
+ @XmlElement(required = false, name = "subscriptionCancellationPolicy")
+ private OverdueCancellationPolicy subscriptionCancellationPolicy = OverdueCancellationPolicy.NONE;
+
+ @XmlElement(required = false, name = "isClearState")
+ private Boolean isClearState = false;
+
+ @XmlElement(required = false, name = "autoReevaluationInterval")
+ private DefaultDuration autoReevaluationInterval;
+
+ @XmlElement(required = false, name = "enterStateEmailNotification")
+ private DefaultEmailNotification enterStateEmailNotification;
+
+ //Other actions could include
+ // - trigger payment retry?
+ // - add tagStore to bundle/account
+ // - set payment failure email template
+ // - set payment retry interval
+ // - backup payment mechanism?
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getExternalMessage() {
+ return externalMessage;
+ }
+
+ @Override
+ public boolean blockChanges() {
+ return blockChanges || disableEntitlement;
+ }
+
+ @Override
+ public boolean disableEntitlementAndChangesBlocked() {
+ return disableEntitlement;
+ }
+
+ @Override
+ public OverdueCancellationPolicy getSubscriptionCancellationPolicy() {
+ return subscriptionCancellationPolicy;
+ }
+
+ @Override
+ public Period getReevaluationInterval() throws OverdueApiException {
+ if (autoReevaluationInterval == null || autoReevaluationInterval.getUnit() == TimeUnit.UNLIMITED || autoReevaluationInterval.getNumber() == 0) {
+ throw new OverdueApiException(ErrorCode.OVERDUE_NO_REEVALUATION_INTERVAL, name);
+ }
+ return autoReevaluationInterval.toJodaPeriod();
+ }
+
+ @Override
+ public DefaultCondition getCondition() {
+ return condition;
+ }
+
+ protected DefaultOverdueState setName(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ protected DefaultOverdueState setClearState(final boolean isClearState) {
+ this.isClearState = isClearState;
+ return this;
+ }
+
+ protected DefaultOverdueState setExternalMessage(final String externalMessage) {
+ this.externalMessage = externalMessage;
+ return this;
+ }
+
+ protected DefaultOverdueState setDisableEntitlement(final boolean cancel) {
+ this.disableEntitlement = cancel;
+ return this;
+ }
+
+ public DefaultOverdueState setSubscriptionCancellationPolicy(final OverdueCancellationPolicy policy) {
+ this.subscriptionCancellationPolicy = policy;
+ return this;
+ }
+
+ protected DefaultOverdueState setBlockChanges(final boolean cancel) {
+ this.blockChanges = cancel;
+ return this;
+ }
+
+ protected DefaultOverdueState setCondition(final DefaultCondition condition) {
+ this.condition = condition;
+ return this;
+ }
+
+ @Override
+ public boolean isClearState() {
+ return isClearState;
+ }
+
+ @Override
+ public ValidationErrors validate(final OverdueConfig root,
+ final ValidationErrors errors) {
+ if (name.length() > MAX_NAME_LENGTH) {
+ errors.add(new ValidationError(String.format("Name of state '%s' exceeds the maximum length of %d", name, MAX_NAME_LENGTH), root.getURI(), DefaultOverdueState.class, name));
+ }
+ return errors;
+ }
+
+ @Override
+ public int getDaysBetweenPaymentRetries() {
+ return 8;
+ }
+
+ @Override
+ public EmailNotification getEnterStateEmailNotification() {
+ return enterStateEmailNotification;
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStateSet.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStateSet.java
new file mode 100644
index 0000000..6c007fb
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStateSet.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
+import org.joda.time.LocalDate;
+import org.joda.time.Period;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.overdue.OverdueApiException;
+import org.killbill.billing.overdue.OverdueState;
+import org.killbill.billing.overdue.config.api.BillingState;
+import org.killbill.billing.overdue.config.api.OverdueStateSet;
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+import org.killbill.billing.junction.DefaultBlockingState;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public abstract class DefaultOverdueStateSet extends ValidatingConfig<OverdueConfig> implements OverdueStateSet {
+
+ private static final Period ZERO_PERIOD = new Period();
+ private final DefaultOverdueState clearState = new DefaultOverdueState().setName(DefaultBlockingState.CLEAR_STATE_NAME).setClearState(true);
+
+ protected abstract DefaultOverdueState[] getStates();
+
+ @Override
+ public OverdueState findState(final String stateName) throws OverdueApiException {
+ if (stateName.equals(DefaultBlockingState.CLEAR_STATE_NAME)) {
+ return clearState;
+ }
+ for (final DefaultOverdueState state : getStates()) {
+ if (state.getName().equals(stateName)) {
+ return state;
+ }
+ }
+ throw new OverdueApiException(ErrorCode.CAT_NO_SUCH_OVEDUE_STATE, stateName);
+ }
+
+
+ /* (non-Javadoc)
+ * @see org.killbill.billing.catalog.overdue.OverdueBillingState#findClearState()
+ */
+ @Override
+ public DefaultOverdueState getClearState() throws OverdueApiException {
+ return clearState;
+ }
+
+ @Override
+ public DefaultOverdueState calculateOverdueState(final BillingState billingState, final LocalDate now) throws OverdueApiException {
+ for (final DefaultOverdueState overdueState : getStates()) {
+ if (overdueState.getCondition() != null && overdueState.getCondition().evaluate(billingState, now)) {
+ return overdueState;
+ }
+ }
+ return getClearState();
+ }
+
+ @Override
+ public ValidationErrors validate(final OverdueConfig root,
+ final ValidationErrors errors) {
+ for (final DefaultOverdueState state : getStates()) {
+ state.validate(root, errors);
+ }
+ try {
+ getClearState();
+ } catch (OverdueApiException e) {
+ if (e.getCode() == ErrorCode.CAT_MISSING_CLEAR_STATE.getCode()) {
+ errors.add("Overdue state set is missing a clear state.",
+ root.getURI(), this.getClass(), "");
+ }
+ }
+
+ return errors;
+ }
+
+ @Override
+ public int size() {
+ return getStates().length;
+ }
+
+ @Override
+ public OverdueState getFirstState() {
+ return getStates()[0];
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/OverdueConfig.java b/overdue/src/main/java/org/killbill/billing/overdue/config/OverdueConfig.java
new file mode 100644
index 0000000..0bc3a48
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/OverdueConfig.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.net.URI;
+
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlRootElement(name = "overdueConfig")
+@XmlAccessorType(XmlAccessType.NONE)
+public class OverdueConfig extends ValidatingConfig<OverdueConfig> {
+
+ @XmlElement(required = true, name = "accountOverdueStates")
+ private OverdueStatesAccount accountOverdueStates = new OverdueStatesAccount();
+
+ public DefaultOverdueStateSet getStateSet() {
+ return accountOverdueStates;
+ }
+
+ @Override
+ public ValidationErrors validate(final OverdueConfig root,
+ final ValidationErrors errors) {
+ return accountOverdueStates.validate(root, errors);
+ }
+
+ public OverdueConfig setOverdueStates(final OverdueStatesAccount accountOverdueStates) {
+ this.accountOverdueStates = accountOverdueStates;
+ return this;
+ }
+
+
+ public URI getURI() {
+ return null;
+ }
+
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/OverdueStatesAccount.java b/overdue/src/main/java/org/killbill/billing/overdue/config/OverdueStatesAccount.java
new file mode 100644
index 0000000..afca345
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/OverdueStatesAccount.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+import javax.xml.bind.annotation.XmlElement;
+
+import org.joda.time.Period;
+
+import org.killbill.billing.catalog.api.TimeUnit;
+
+public class OverdueStatesAccount extends DefaultOverdueStateSet {
+
+ @XmlElement(required = false, name = "initialReevaluationInterval")
+ private DefaultDuration initialReevaluationInterval;
+
+ @SuppressWarnings("unchecked")
+ @XmlElement(required = true, name = "state")
+ private DefaultOverdueState[] accountOverdueStates = new DefaultOverdueState[0];
+
+ @Override
+ protected DefaultOverdueState[] getStates() {
+ return accountOverdueStates;
+ }
+
+ @Override
+ public Period getInitialReevaluationInterval() {
+ if (initialReevaluationInterval == null || initialReevaluationInterval.getUnit() == TimeUnit.UNLIMITED || initialReevaluationInterval.getNumber() == 0) {
+ return null;
+ }
+ return initialReevaluationInterval.toJodaPeriod();
+ }
+
+ protected OverdueStatesAccount setAccountOverdueStates(final DefaultOverdueState[] accountOverdueStates) {
+ this.accountOverdueStates = accountOverdueStates;
+ return this;
+ }
+
+ protected OverdueStatesAccount setInitialReevaluationInterval(final DefaultDuration initialReevaluationInterval) {
+ this.initialReevaluationInterval = initialReevaluationInterval;
+ return this;
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/CreateOverdueConfigSchema.java b/overdue/src/main/java/org/killbill/billing/overdue/CreateOverdueConfigSchema.java
new file mode 100644
index 0000000..9effa3c
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/CreateOverdueConfigSchema.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.Writer;
+
+import org.killbill.billing.overdue.config.OverdueConfig;
+import org.killbill.billing.util.config.catalog.XMLSchemaGenerator;
+
+public class CreateOverdueConfigSchema {
+
+ /**
+ * @param args output file path
+ */
+ public static void main(final String[] args) throws Exception {
+ if (args.length != 1) {
+ System.err.println("Usage: <filepath>");
+ System.exit(0);
+ }
+
+ final File f = new File(args[0]);
+ final Writer w = new FileWriter(f);
+ w.write(XMLSchemaGenerator.xmlSchemaAsString(OverdueConfig.class));
+ w.close();
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/exceptions/OverdueError.java b/overdue/src/main/java/org/killbill/billing/overdue/exceptions/OverdueError.java
new file mode 100644
index 0000000..629a3cc
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/exceptions/OverdueError.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.exceptions;
+
+public class OverdueError extends Error {
+
+ private static final long serialVersionUID = 131398536;
+
+ public OverdueError() {
+ super();
+ }
+
+ public OverdueError(final String msg, final Throwable arg1) {
+ super(msg, arg1);
+ }
+
+ public OverdueError(final String msg) {
+ super(msg);
+ }
+
+ public OverdueError(final Throwable msg) {
+ super(msg);
+ }
+}
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
new file mode 100644
index 0000000..5dda68d
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/glue/DefaultOverdueModule.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.glue.OverdueModule;
+import org.killbill.billing.overdue.notification.OverdueAsyncBusNotifier;
+import org.killbill.billing.overdue.notification.OverdueAsyncBusPoster;
+import org.killbill.billing.overdue.notification.OverdueCheckNotifier;
+import org.killbill.billing.overdue.notification.OverdueCheckPoster;
+import org.killbill.billing.overdue.notification.OverduePoster;
+import org.killbill.billing.overdue.notification.OverdueNotifier;
+import org.killbill.billing.overdue.OverdueProperties;
+import org.killbill.billing.overdue.OverdueService;
+import org.killbill.billing.overdue.OverdueUserApi;
+import org.killbill.billing.overdue.api.DefaultOverdueUserApi;
+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.service.DefaultOverdueService;
+import org.killbill.billing.overdue.wrapper.OverdueWrapperFactory;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+public class DefaultOverdueModule extends AbstractModule implements OverdueModule {
+
+ protected final ConfigSource configSource;
+
+ public static final String OVERDUE_NOTIFIER_CHECK_NAMED = "overdueNotifierCheck";
+ public static final String OVERDUE_NOTIFIER_ASYNC_BUS_NAMED = "overdueNotifierAsyncBus";
+
+ public DefaultOverdueModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ @Override
+ protected void configure() {
+ installOverdueUserApi();
+
+ // internal bindings
+ installOverdueService();
+ installOverdueWrapperFactory();
+ installOverdueEmail();
+
+ final OverdueProperties config = new ConfigurationObjectFactory(configSource).build(OverdueProperties.class);
+ bind(OverdueProperties.class).toInstance(config);
+
+ bind(OverdueNotifier.class).annotatedWith(Names.named(OVERDUE_NOTIFIER_CHECK_NAMED)).to(OverdueCheckNotifier.class).asEagerSingleton();
+ bind(OverdueNotifier.class).annotatedWith(Names.named(OVERDUE_NOTIFIER_ASYNC_BUS_NAMED)).to(OverdueAsyncBusNotifier.class).asEagerSingleton();
+
+ bind(OverduePoster.class).annotatedWith(Names.named(OVERDUE_NOTIFIER_CHECK_NAMED)).to(OverdueCheckPoster.class).asEagerSingleton();
+ bind(OverduePoster.class).annotatedWith(Names.named(OVERDUE_NOTIFIER_ASYNC_BUS_NAMED)).to(OverdueAsyncBusPoster.class).asEagerSingleton();
+ }
+
+ protected void installOverdueService() {
+ bind(OverdueService.class).to(DefaultOverdueService.class).asEagerSingleton();
+ }
+
+ protected void installOverdueWrapperFactory() {
+ bind(OverdueWrapperFactory.class).asEagerSingleton();
+ }
+
+ protected void installOverdueEmail() {
+ bind(OverdueEmailFormatterFactory.class).to(DefaultOverdueEmailFormatterFactory.class).asEagerSingleton();
+ bind(OverdueEmailGenerator.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installOverdueUserApi() {
+ bind(OverdueUserApi.class).to(DefaultOverdueUserApi.class).asEagerSingleton();
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueDispatcher.java b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueDispatcher.java
new file mode 100644
index 0000000..58653d2
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueDispatcher.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.listener;
+
+import java.util.UUID;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.overdue.wrapper.OverdueWrapperFactory;
+import org.killbill.billing.callcontext.InternalCallContext;
+
+import com.google.inject.Inject;
+
+public class OverdueDispatcher {
+
+ Logger log = LoggerFactory.getLogger(OverdueDispatcher.class);
+
+ private final OverdueWrapperFactory factory;
+
+ @Inject
+ public OverdueDispatcher(final OverdueWrapperFactory factory) {
+ this.factory = factory;
+ }
+
+ public void processOverdueForAccount(final UUID accountId, final InternalCallContext context) {
+ processOverdue(accountId, context);
+ }
+
+ public void clearOverdueForAccount(final UUID accountId, final InternalCallContext context) {
+ clearOverdue(accountId, context);
+ }
+
+ private void processOverdue(final UUID accountId, final InternalCallContext context) {
+ try {
+ factory.createOverdueWrapperFor(accountId, context).refresh(context);
+ } catch (BillingExceptionBase e) {
+ log.error(String.format("Error processing Overdue for blockable %s", accountId), e);
+ }
+ }
+
+ private void clearOverdue(final UUID accountId, final InternalCallContext context) {
+ try {
+ factory.createOverdueWrapperFor(accountId, context).clear(context);
+ } catch (BillingExceptionBase e) {
+ log.error(String.format("Error processing Overdue for blockable %s (type %s)", accountId), e);
+ }
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
new file mode 100644
index 0000000..dfb24f2
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.listener;
+
+import java.util.UUID;
+
+import javax.inject.Named;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.bus.api.BusEvent;
+import org.killbill.clock.Clock;
+import org.killbill.billing.overdue.notification.OverdueAsyncBusNotificationKey;
+import org.killbill.billing.overdue.notification.OverdueAsyncBusNotificationKey.OverdueAsyncBusNotificationAction;
+import org.killbill.billing.overdue.notification.OverdueAsyncBusNotifier;
+import org.killbill.billing.overdue.notification.OverduePoster;
+import org.killbill.billing.overdue.glue.DefaultOverdueModule;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.events.ControlTagCreationInternalEvent;
+import org.killbill.billing.events.ControlTagDeletionInternalEvent;
+import org.killbill.billing.events.InvoiceAdjustmentInternalEvent;
+import org.killbill.billing.events.PaymentErrorInternalEvent;
+import org.killbill.billing.events.PaymentInfoInternalEvent;
+import org.killbill.billing.util.tag.ControlTagType;
+
+import com.google.common.eventbus.Subscribe;
+import com.google.inject.Inject;
+
+public class OverdueListener {
+
+ private final OverdueDispatcher dispatcher;
+ private final InternalCallContextFactory internalCallContextFactory;
+ private final OverduePoster asyncPoster;
+ private final Clock clock;
+
+ private static final Logger log = LoggerFactory.getLogger(OverdueListener.class);
+
+ @Inject
+ public OverdueListener(final OverdueDispatcher dispatcher,
+ final Clock clock,
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_ASYNC_BUS_NAMED)final OverduePoster asyncPoster,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.dispatcher = dispatcher;
+ this.asyncPoster = asyncPoster;
+ this.clock = clock;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Subscribe
+ public void handle_OVERDUE_ENFORCEMENT_OFF_Insert(final ControlTagCreationInternalEvent event) {
+ if (event.getTagDefinition().getName().equals(ControlTagType.OVERDUE_ENFORCEMENT_OFF.toString()) && event.getObjectType() == ObjectType.ACCOUNT) {
+ insertBusEventIntoNotificationQueue(event.getObjectId(), event, OverdueAsyncBusNotificationAction.CLEAR);
+ }
+ }
+
+ @Subscribe
+ public void handle_OVERDUE_ENFORCEMENT_OFF_Removal(final ControlTagDeletionInternalEvent event) {
+ if (event.getTagDefinition().getName().equals(ControlTagType.OVERDUE_ENFORCEMENT_OFF.toString()) && event.getObjectType() == ObjectType.ACCOUNT) {
+ insertBusEventIntoNotificationQueue(event.getObjectId(), event, OverdueAsyncBusNotificationAction.REFRESH);
+ }
+ }
+
+
+ @Subscribe
+ public void handlePaymentInfoEvent(final PaymentInfoInternalEvent event) {
+ log.debug("Received PaymentInfo event {}", event);
+ insertBusEventIntoNotificationQueue(event.getAccountId(), event, OverdueAsyncBusNotificationAction.REFRESH);
+ }
+
+ @Subscribe
+ public void handlePaymentErrorEvent(final PaymentErrorInternalEvent event) {
+ log.debug("Received PaymentError event {}", event);
+ insertBusEventIntoNotificationQueue(event.getAccountId(), event, OverdueAsyncBusNotificationAction.REFRESH);
+ }
+
+ @Subscribe
+ public void handleInvoiceAdjustmentEvent(final InvoiceAdjustmentInternalEvent event) {
+ log.debug("Received InvoiceAdjustment event {}", event);
+ insertBusEventIntoNotificationQueue(event.getAccountId(), event, OverdueAsyncBusNotificationAction.REFRESH);
+ }
+
+ private void insertBusEventIntoNotificationQueue(final UUID accountId, final BusEvent event, final OverdueAsyncBusNotificationAction action) {
+ final OverdueAsyncBusNotificationKey notificationKey = new OverdueAsyncBusNotificationKey(accountId, action);
+ asyncPoster.insertOverdueNotification(accountId, clock.getUTCNow(), OverdueAsyncBusNotifier.OVERDUE_ASYNC_BUS_NOTIFIER_QUEUE, notificationKey, createCallContext(event.getUserToken(), event.getSearchKey1(), event.getSearchKey2()));
+
+ }
+
+ private InternalCallContext createCallContext(final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ return internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "OverdueService", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverdueNotifierBase.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverdueNotifierBase.java
new file mode 100644
index 0000000..d66432b
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverdueNotifierBase.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueHandler;
+import org.killbill.billing.overdue.OverdueProperties;
+import org.killbill.billing.overdue.listener.OverdueDispatcher;
+import org.killbill.billing.overdue.service.DefaultOverdueService;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+
+public abstract class DefaultOverdueNotifierBase implements OverdueNotifier {
+
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultOverdueNotifierBase.class);
+
+ private final InternalCallContextFactory internalCallContextFactory;
+ protected final NotificationQueueService notificationQueueService;
+ protected final OverdueProperties config;
+ protected final OverdueDispatcher dispatcher;
+ protected NotificationQueue overdueQueue;
+
+ public abstract String getQueueName();
+
+ public abstract void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDate, final UUID userToken, final Long accountRecordId, final Long tenantRecordId);
+
+ public DefaultOverdueNotifierBase(final NotificationQueueService notificationQueueService,
+ final OverdueProperties config,
+ final InternalCallContextFactory internalCallContextFactory,
+ final OverdueDispatcher dispatcher) {
+ this.notificationQueueService = notificationQueueService;
+ this.config = config;
+ this.dispatcher = dispatcher;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public void initialize() {
+
+ final OverdueNotifier myself = this;
+
+ final NotificationQueueHandler notificationQueueHandler = new NotificationQueueHandler() {
+ @Override
+ public void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDate, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ myself.handleReadyNotification(notificationKey, eventDate, userToken, accountRecordId, tenantRecordId);
+ }
+ };
+
+ try {
+ overdueQueue = notificationQueueService.createNotificationQueue(DefaultOverdueService.OVERDUE_SERVICE_NAME,
+ getQueueName(),
+ notificationQueueHandler);
+ } catch (NotificationQueueAlreadyExists e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void start() {
+ overdueQueue.startQueue();
+ }
+
+ @Override
+ public void stop() {
+ if (overdueQueue != null) {
+ overdueQueue.stopQueue();
+ try {
+ notificationQueueService.deleteNotificationQueue(overdueQueue.getServiceName(), overdueQueue.getQueueName());
+ } catch (NoSuchNotificationQueue e) {
+ log.error("Error deleting a queue by its own name - this should never happen", e);
+ }
+ }
+ }
+
+ protected InternalCallContext createCallContext(final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ return internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "OverdueService", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
+ }
+
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java
new file mode 100644
index 0000000..42e54a7
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/DefaultOverduePosterBase.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.IDBI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.clock.Clock;
+import org.killbill.notificationq.api.NotificationEventWithMetadata;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.billing.overdue.service.DefaultOverdueService;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+
+public abstract class DefaultOverduePosterBase implements OverduePoster {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultOverduePosterBase.class);
+
+ private final NotificationQueueService notificationQueueService;
+ private final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
+
+ public DefaultOverduePosterBase(final NotificationQueueService notificationQueueService,
+ final IDBI dbi, final Clock clock,
+ final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ this.notificationQueueService = notificationQueueService;
+ this.transactionalSqlDao = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao);
+ }
+
+ @Override
+ public <T extends OverdueCheckNotificationKey> void insertOverdueNotification(final UUID accountId, final DateTime futureNotificationTime, final String overdueQueueName, final T notificationKey, final InternalCallContext context) {
+
+ try {
+ final NotificationQueue overdueQueue = notificationQueueService.getNotificationQueue(DefaultOverdueService.OVERDUE_SERVICE_NAME,
+ overdueQueueName);
+
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+
+ // Check if we already have notifications for that key
+ final Class<T> clazz = (Class<T>) notificationKey.getClass();
+ final Collection<NotificationEventWithMetadata<T>> futureNotifications = getFutureNotificationsForAccountInTransaction(entitySqlDaoWrapperFactory, overdueQueue, accountId,
+ clazz, context);
+
+ boolean shouldInsertNewNotification = cleanupFutureNotificationsFormTransaction(entitySqlDaoWrapperFactory, futureNotifications, futureNotificationTime, overdueQueue);
+ if (shouldInsertNewNotification) {
+ log.debug("Queuing overdue check notification. Account id: {}, timestamp: {}", accountId.toString(), futureNotificationTime.toString());
+ overdueQueue.recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory.getSqlDao(), futureNotificationTime, notificationKey, context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
+ } else {
+ log.debug("Skipping queuing overdue check notification. Account id: {}, timestamp: {}", accountId.toString(), futureNotificationTime.toString());
+ }
+ return null;
+ }
+ });
+ } catch (NoSuchNotificationQueue e) {
+ log.error("Attempting to put items on a non-existent queue (DefaultOverdueCheck).", e);
+ return;
+ }
+ }
+
+
+ @Override
+ public <T extends OverdueCheckNotificationKey> void clearOverdueCheckNotifications(final UUID accountId, final String overdueQueueName, final Class<T> clazz, final InternalCallContext context) {
+ try {
+ final NotificationQueue checkOverdueQueue = notificationQueueService.getNotificationQueue(DefaultOverdueService.OVERDUE_SERVICE_NAME,
+ overdueQueueName);
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final Collection<NotificationEventWithMetadata<T>> futureNotifications = getFutureNotificationsForAccountInTransaction(entitySqlDaoWrapperFactory, checkOverdueQueue, accountId,
+ clazz, context);
+ for (final NotificationEventWithMetadata<T> notification : futureNotifications) {
+ checkOverdueQueue.removeNotificationFromTransaction(entitySqlDaoWrapperFactory.getSqlDao(), notification.getRecordId());
+ }
+ return null;
+ }
+ });
+ } catch (NoSuchNotificationQueue e) {
+ log.error("Attempting to clear items from a non-existent queue (DefaultOverdueCheck).", e);
+ }
+ }
+
+ @VisibleForTesting
+ <T extends OverdueCheckNotificationKey> Collection<NotificationEventWithMetadata<T>> getFutureNotificationsForAccountInTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory,
+ final NotificationQueue checkOverdueQueue,
+ final UUID accountId,
+ final Class<T> clazz,
+ final InternalCallContext context) {
+
+ final List<NotificationEventWithMetadata<T>> notifications = checkOverdueQueue.getFutureNotificationFromTransactionForSearchKey1(clazz, context.getAccountRecordId(), entitySqlDaoWrapperFactory.getSqlDao());
+
+ /*
+ final Collection<NotificationEventWithMetadata<T>> notificationsFiltered = Collections2.filter(notifications, new Predicate<NotificationEventWithMetadata<T>>() {
+ @Override
+ public boolean apply(@Nullable final NotificationEventWithMetadata<T> input) {
+ final OverdueCheckNotificationKey notificationKey = input.getEvent();
+ return (accountId.equals(notificationKey.getUuidKey()));
+ }
+ });
+ return notificationsFiltered;
+ */
+ return notifications;
+ }
+
+
+ protected abstract <T extends OverdueCheckNotificationKey> boolean cleanupFutureNotificationsFormTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory,
+ final Collection<NotificationEventWithMetadata<T>> futureNotifications,
+ final DateTime futureNotificationTime, final NotificationQueue overdueQueue);
+
+
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusNotificationKey.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusNotificationKey.java
new file mode 100644
index 0000000..073bd36
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusNotificationKey.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.UUID;
+
+import org.killbill.notificationq.api.NotificationEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class OverdueAsyncBusNotificationKey extends OverdueCheckNotificationKey implements NotificationEvent {
+
+ private final OverdueAsyncBusNotificationAction action;
+
+ public enum OverdueAsyncBusNotificationAction {
+ REFRESH,
+ CLEAR
+ }
+
+ @JsonCreator
+ public OverdueAsyncBusNotificationKey(@JsonProperty("uuidKey") final UUID uuidKey,
+ @JsonProperty("action") final OverdueAsyncBusNotificationAction action) {
+ super(uuidKey);
+ this.action = action;
+ }
+
+
+ public OverdueAsyncBusNotificationAction getAction() {
+ return action;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof OverdueAsyncBusNotificationKey)) {
+ return false;
+ }
+
+ final OverdueAsyncBusNotificationKey that = (OverdueAsyncBusNotificationKey) o;
+
+ if (action != that.action) {
+ return false;
+ }
+ if (getUuidKey() != null ? !getUuidKey().equals(that.getUuidKey()) : that.getUuidKey() != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = getUuidKey() != null ? getUuidKey().hashCode() : 0;
+ result = 31 * result + (action != null ? action.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusNotifier.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusNotifier.java
new file mode 100644
index 0000000..9aca856
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusNotifier.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.overdue.OverdueProperties;
+import org.killbill.billing.overdue.listener.OverdueDispatcher;
+import org.killbill.billing.overdue.listener.OverdueListener;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+
+import com.google.inject.Inject;
+
+public class OverdueAsyncBusNotifier extends DefaultOverdueNotifierBase implements OverdueNotifier {
+
+ private static final Logger log = LoggerFactory.getLogger(OverdueCheckNotifier.class);
+
+ public static final String OVERDUE_ASYNC_BUS_NOTIFIER_QUEUE = "overdue-async-bus-queue";
+
+
+ @Inject
+ public OverdueAsyncBusNotifier(final NotificationQueueService notificationQueueService, final OverdueProperties config,
+ final InternalCallContextFactory internalCallContextFactory,
+ final OverdueDispatcher dispatcher) {
+ super(notificationQueueService, config, internalCallContextFactory, dispatcher);
+ }
+
+ @Override
+ public String getQueueName() {
+ return OVERDUE_ASYNC_BUS_NOTIFIER_QUEUE;
+ }
+
+ @Override
+ public void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDate, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ try {
+ if (!(notificationKey instanceof OverdueAsyncBusNotificationKey)) {
+ log.error("Overdue service received Unexpected notificationKey {}", notificationKey.getClass().getName());
+ return;
+ }
+
+ final OverdueAsyncBusNotificationKey key = (OverdueAsyncBusNotificationKey) notificationKey;
+ switch (key.getAction()) {
+ case CLEAR:
+ dispatcher.clearOverdueForAccount(key.getUuidKey(), createCallContext(userToken, accountRecordId, tenantRecordId));
+ break;
+ case REFRESH:
+ dispatcher.processOverdueForAccount(key.getUuidKey(), createCallContext(userToken, accountRecordId, tenantRecordId));
+ break;
+ default:
+ throw new RuntimeException("Unexpected action " + key.getAction() + " for account " + key.getUuidKey());
+ }
+ } catch (IllegalArgumentException e) {
+ log.error("The key returned from the queue " + OVERDUE_ASYNC_BUS_NOTIFIER_QUEUE + " does not contain a valid UUID", e);
+ }
+ }
+
+
+}
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
new file mode 100644
index 0000000..0a29f57
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueAsyncBusPoster.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.Collection;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.clock.Clock;
+import org.killbill.notificationq.api.NotificationEventWithMetadata;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.inject.Inject;
+
+public class OverdueAsyncBusPoster extends DefaultOverduePosterBase {
+
+ @Inject
+ public OverdueAsyncBusPoster(final NotificationQueueService notificationQueueService,
+ final IDBI dbi, final Clock clock,
+ final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ super(notificationQueueService, dbi, clock, cacheControllerDispatcher, nonEntityDao);
+ }
+
+ @Override
+ protected <T extends OverdueCheckNotificationKey> boolean cleanupFutureNotificationsFormTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory,
+ final Collection<NotificationEventWithMetadata<T>> futureNotifications,
+ final DateTime futureNotificationTime,
+ final NotificationQueue overdueQueue) {
+ // If we already have notification for that account we don't insert the new one
+ // 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
+ //
+ return futureNotifications.size() == 0;
+ }
+
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckNotificationKey.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckNotificationKey.java
new file mode 100644
index 0000000..f129120
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckNotificationKey.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.UUID;
+
+import org.killbill.notificationq.DefaultUUIDNotificationKey;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class OverdueCheckNotificationKey extends DefaultUUIDNotificationKey {
+
+ @JsonCreator
+ public OverdueCheckNotificationKey(@JsonProperty("uuidKey") final UUID uuidKey) {
+ super(uuidKey);
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckNotifier.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckNotifier.java
new file mode 100644
index 0000000..22417cd
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckNotifier.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.overdue.OverdueProperties;
+import org.killbill.billing.overdue.listener.OverdueDispatcher;
+import org.killbill.billing.overdue.listener.OverdueListener;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+
+import com.google.inject.Inject;
+
+public class OverdueCheckNotifier extends DefaultOverdueNotifierBase implements OverdueNotifier {
+
+ private static final Logger log = LoggerFactory.getLogger(OverdueCheckNotifier.class);
+
+ public static final String OVERDUE_CHECK_NOTIFIER_QUEUE = "overdue-check-queue";
+
+
+ @Inject
+ public OverdueCheckNotifier(final NotificationQueueService notificationQueueService, final OverdueProperties config,
+ final InternalCallContextFactory internalCallContextFactory,
+ final OverdueDispatcher dispatcher) {
+ super(notificationQueueService, config, internalCallContextFactory, dispatcher);
+ }
+
+ @Override
+ public String getQueueName() {
+ return OVERDUE_CHECK_NOTIFIER_QUEUE;
+ }
+
+ @Override
+ public void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDate, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ try {
+ if (!(notificationKey instanceof OverdueCheckNotificationKey)) {
+ log.error("Overdue service received Unexpected notificationKey {}", notificationKey.getClass().getName());
+ return;
+ }
+
+ final OverdueCheckNotificationKey key = (OverdueCheckNotificationKey) notificationKey;
+ dispatcher.processOverdueForAccount(key.getUuidKey(), createCallContext(userToken, accountRecordId, tenantRecordId));
+ } catch (IllegalArgumentException e) {
+ log.error("The key returned from the NextBillingNotificationQueue is not a valid UUID", e);
+ }
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java
new file mode 100644
index 0000000..f01094f
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueCheckPoster.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.clock.Clock;
+import org.killbill.notificationq.api.NotificationEventWithMetadata;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.inject.Inject;
+
+public class OverdueCheckPoster extends DefaultOverduePosterBase {
+
+ @Inject
+ public OverdueCheckPoster(final NotificationQueueService notificationQueueService,
+ final IDBI dbi, final Clock clock,
+ final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ super(notificationQueueService, dbi, clock, cacheControllerDispatcher, nonEntityDao);
+ }
+
+ @Override
+ protected <T extends OverdueCheckNotificationKey> boolean cleanupFutureNotificationsFormTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory,
+ final Collection<NotificationEventWithMetadata<T>> futureNotifications,
+ final DateTime futureNotificationTime, final NotificationQueue overdueQueue) {
+
+ boolean shouldInsertNewNotification = true;
+ if (futureNotifications.size() > 0) {
+ // Results are ordered by effective date asc
+ final DateTime earliestExistingNotificationDate = futureNotifications.iterator().next().getEffectiveDate();
+
+ final int minIndexToDeleteFrom;
+ if (earliestExistingNotificationDate.isBefore(futureNotificationTime)) {
+ // We don't have to insert a new one. For sanity, delete any other future notification
+ minIndexToDeleteFrom = 1;
+ shouldInsertNewNotification = false;
+ } else {
+ // We win - we are before any other already recorded. Delete all others.
+ minIndexToDeleteFrom = 0;
+ }
+
+ int index = 0;
+ final Iterator<NotificationEventWithMetadata<T>> it = futureNotifications.iterator();
+ while (it.hasNext()) {
+ final NotificationEventWithMetadata<T> cur = it.next();
+ if (minIndexToDeleteFrom <= index) {
+ overdueQueue.removeNotificationFromTransaction(entitySqlDaoWrapperFactory.getSqlDao(), cur.getRecordId());
+ }
+ index++;
+ }
+ }
+ return shouldInsertNewNotification;
+ }
+
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueNotifier.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueNotifier.java
new file mode 100644
index 0000000..e1b14ba
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverdueNotifier.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.notificationq.api.NotificationEvent;
+
+public interface OverdueNotifier {
+
+ public void initialize();
+
+ public void start();
+
+ public void stop();
+
+ public abstract void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDate, final UUID userToken, final Long accountRecordId, final Long tenantRecordId);
+
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/notification/OverduePoster.java b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverduePoster.java
new file mode 100644
index 0000000..517f17b
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/notification/OverduePoster.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+
+public interface OverduePoster {
+
+ public <T extends OverdueCheckNotificationKey> void insertOverdueNotification(final UUID accountId, final DateTime futureNotificationTime, final String overdueQueueName, final T notificationKey, final InternalCallContext context);
+
+ public <T extends OverdueCheckNotificationKey> void clearOverdueCheckNotifications(UUID accountId, final String overdueQueueName, final Class<T> clazz, final InternalCallContext context);
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/OverdueProperties.java b/overdue/src/main/java/org/killbill/billing/overdue/OverdueProperties.java
new file mode 100644
index 0000000..43b18b9
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/OverdueProperties.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+import org.killbill.billing.util.config.KillbillConfig;
+
+public interface OverdueProperties extends KillbillConfig {
+
+ @Config("killbill.overdue.uri")
+ @Default("NoOverdueConfig.xml")
+ @Description("Overdue configuration location. Either in the classpath or in the filesystem")
+ public String getConfigURI();
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/service/DefaultOverdueService.java b/overdue/src/main/java/org/killbill/billing/overdue/service/DefaultOverdueService.java
new file mode 100644
index 0000000..bbe603d
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/service/DefaultOverdueService.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.service;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import javax.inject.Named;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.bus.api.PersistentBus.EventBusException;
+import org.killbill.billing.lifecycle.LifecycleHandlerType;
+import org.killbill.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+import org.killbill.billing.overdue.notification.OverdueNotifier;
+import org.killbill.billing.overdue.OverdueProperties;
+import org.killbill.billing.overdue.OverdueService;
+import org.killbill.billing.overdue.OverdueUserApi;
+import org.killbill.billing.overdue.api.DefaultOverdueUserApi;
+import org.killbill.billing.overdue.config.OverdueConfig;
+import org.killbill.billing.overdue.glue.DefaultOverdueModule;
+import org.killbill.billing.overdue.listener.OverdueListener;
+import org.killbill.billing.overdue.wrapper.OverdueWrapperFactory;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.inject.Inject;
+
+public class DefaultOverdueService implements OverdueService {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultOverdueService.class);
+
+ public static final String OVERDUE_SERVICE_NAME = "overdue-service";
+
+ private final OverdueUserApi userApi;
+ private final OverdueProperties properties;
+ private final OverdueNotifier asyncNotifier;
+ private final OverdueNotifier checkNotifier;
+ private final BusService busService;
+ private final OverdueListener listener;
+ private final OverdueWrapperFactory factory;
+
+ private OverdueConfig overdueConfig;
+ private boolean isConfigLoaded;
+
+ @Inject
+ public DefaultOverdueService(
+ final OverdueUserApi userApi,
+ final OverdueProperties properties,
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_CHECK_NAMED) final OverdueNotifier checkNotifier,
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_ASYNC_BUS_NAMED) final OverdueNotifier asyncNotifier,
+ final BusService busService,
+ final OverdueListener listener,
+ final OverdueWrapperFactory factory) {
+ this.userApi = userApi;
+ this.properties = properties;
+ this.checkNotifier = checkNotifier;
+ this.asyncNotifier = asyncNotifier;
+ this.busService = busService;
+ this.listener = listener;
+ this.factory = factory;
+ this.isConfigLoaded = false;
+ }
+
+ @Override
+ public String getName() {
+ return OVERDUE_SERVICE_NAME;
+ }
+
+ @Override
+ public OverdueUserApi getUserApi() {
+ return userApi;
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.LOAD_CATALOG)
+ public synchronized void loadConfig() throws ServiceException {
+ if (!isConfigLoaded) {
+ try {
+ final URI u = new URI(properties.getConfigURI());
+ overdueConfig = XMLLoader.getObjectFromUri(u, OverdueConfig.class);
+ // File not found?
+ if (overdueConfig == null) {
+ log.warn("Unable to load the overdue config from " + properties.getConfigURI());
+ overdueConfig = new OverdueConfig();
+ }
+
+ isConfigLoaded = true;
+ } catch (final URISyntaxException e) {
+ overdueConfig = new OverdueConfig();
+ } catch (final IllegalArgumentException e) {
+ overdueConfig = new OverdueConfig();
+ } catch (final Exception e) {
+ throw new ServiceException(e);
+ }
+
+ factory.setOverdueConfig(overdueConfig);
+ ((DefaultOverdueUserApi) userApi).setOverdueConfig(overdueConfig);
+ }
+ }
+
+ @LifecycleHandlerType(LifecycleHandlerType.LifecycleLevel.INIT_SERVICE)
+ public void initialize() {
+ registerForBus();
+ checkNotifier.initialize();
+ asyncNotifier.initialize();
+ }
+
+ private void registerForBus() {
+ try {
+ busService.getBus().register(listener);
+ } catch (final EventBusException e) {
+ log.error("Problem encountered registering OverdueListener on the Event Bus", e);
+ }
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.START_SERVICE)
+ public void start() {
+ checkNotifier.start();
+ asyncNotifier.start();
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE)
+ public void stop() {
+ try {
+ busService.getBus().unregister(listener);
+ } catch (final EventBusException e) {
+ log.error("Problem encountered registering OverdueListener on the Event Bus", e);
+ }
+ checkNotifier.stop();
+ asyncNotifier.stop();
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
new file mode 100644
index 0000000..70e572c
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapper.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.wrapper;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.overdue.OverdueApiException;
+import org.killbill.billing.overdue.OverdueService;
+import org.killbill.billing.overdue.OverdueState;
+import org.killbill.billing.overdue.applicator.OverdueStateApplicator;
+import org.killbill.billing.overdue.calculator.BillingStateCalculator;
+import org.killbill.billing.overdue.config.api.BillingState;
+import org.killbill.billing.overdue.config.api.OverdueException;
+import org.killbill.billing.overdue.config.api.OverdueStateSet;
+
+public class OverdueWrapper {
+
+ private final Account overdueable;
+ private final BlockingInternalApi api;
+ private final Clock clock;
+ private final OverdueStateSet overdueStateSet;
+ private final BillingStateCalculator billingStateCalcuator;
+ private final OverdueStateApplicator overdueStateApplicator;
+
+ public OverdueWrapper(final Account overdueable, final BlockingInternalApi api,
+ final OverdueStateSet overdueStateSet,
+ final Clock clock,
+ final BillingStateCalculator billingStateCalcuator,
+ final OverdueStateApplicator overdueStateApplicator) {
+ this.overdueable = overdueable;
+ this.overdueStateSet = overdueStateSet;
+ this.api = api;
+ this.clock = clock;
+ this.billingStateCalcuator = billingStateCalcuator;
+ this.overdueStateApplicator = overdueStateApplicator;
+ }
+
+ public OverdueState refresh(final InternalCallContext context) throws OverdueException, OverdueApiException {
+ if (overdueStateSet.size() < 1) { // No configuration available
+ return overdueStateSet.getClearState();
+ }
+
+ final BillingState billingState = billingState(context);
+ final String previousOverdueStateName = api.getBlockingStateForService(overdueable.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, context).getStateName();
+
+ final OverdueState currentOverdueState = overdueStateSet.findState(previousOverdueStateName);
+ final OverdueState nextOverdueState = overdueStateSet.calculateOverdueState(billingState, clock.getToday(billingState.getAccountTimeZone()));
+
+ overdueStateApplicator.apply(overdueStateSet, billingState, overdueable, currentOverdueState, nextOverdueState, context);
+
+ return nextOverdueState;
+ }
+
+ public void clear(final InternalCallContext context) throws OverdueException, OverdueApiException {
+ final String previousOverdueStateName = api.getBlockingStateForService(overdueable.getId(), BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, context).getStateName();
+ final OverdueState previousOverdueState = overdueStateSet.findState(previousOverdueStateName);
+ overdueStateApplicator.clear(overdueable, previousOverdueState, overdueStateSet.getClearState(), context);
+ }
+
+ public BillingState billingState(final InternalTenantContext context) throws OverdueException {
+ return billingStateCalcuator.calculateBillingState(overdueable, context);
+ }
+}
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java
new file mode 100644
index 0000000..72e6250
--- /dev/null
+++ b/overdue/src/main/java/org/killbill/billing/overdue/wrapper/OverdueWrapperFactory.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.wrapper;
+
+import java.util.UUID;
+
+import org.joda.time.Period;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.clock.Clock;
+import org.killbill.billing.overdue.applicator.OverdueStateApplicator;
+import org.killbill.billing.overdue.calculator.BillingStateCalculator;
+import org.killbill.billing.overdue.config.DefaultDuration;
+import org.killbill.billing.overdue.config.DefaultOverdueState;
+import org.killbill.billing.overdue.config.DefaultOverdueStateSet;
+import org.killbill.billing.overdue.config.OverdueConfig;
+import org.killbill.billing.overdue.config.api.OverdueException;
+import org.killbill.billing.overdue.config.api.OverdueStateSet;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.junction.BlockingInternalApi;
+
+import com.google.inject.Inject;
+
+public class OverdueWrapperFactory {
+
+ private static final Logger log = LoggerFactory.getLogger(OverdueWrapperFactory.class);
+
+ private final AccountInternalApi accountApi;
+ private final BillingStateCalculator billingStateCalculator;
+ private final OverdueStateApplicator overdueStateApplicator;
+ private final BlockingInternalApi api;
+ private final Clock clock;
+ private OverdueConfig config;
+
+ @Inject
+ public OverdueWrapperFactory(final BlockingInternalApi api, final Clock clock,
+ final BillingStateCalculator billingStateCalculator,
+ final OverdueStateApplicator overdueStateApplicatorBundle,
+ final AccountInternalApi accountApi) {
+ this.billingStateCalculator = billingStateCalculator;
+ this.overdueStateApplicator = overdueStateApplicatorBundle;
+ this.accountApi = accountApi;
+ this.api = api;
+ this.clock = clock;
+ }
+
+ @SuppressWarnings("unchecked")
+ public OverdueWrapper createOverdueWrapperFor(final Account blockable) throws OverdueException {
+ return (OverdueWrapper) new OverdueWrapper(blockable, api, getOverdueStateSet(),
+ clock, billingStateCalculator, overdueStateApplicator);
+ }
+
+ @SuppressWarnings("unchecked")
+ public OverdueWrapper createOverdueWrapperFor(final UUID id, final InternalTenantContext context) throws OverdueException {
+
+ try {
+ Account account = accountApi.getAccountById(id, context);
+ return new OverdueWrapper(account, api, getOverdueStateSet(),
+ clock, billingStateCalculator, overdueStateApplicator);
+
+ } catch (AccountApiException e) {
+ throw new OverdueException(e);
+ }
+ }
+
+ private OverdueStateSet getOverdueStateSet() {
+ if (config == null || config.getStateSet() == null) {
+ return new DefaultOverdueStateSet() {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected DefaultOverdueState[] getStates() {
+ return new DefaultOverdueState[0];
+ }
+
+ @Override
+ public Period getInitialReevaluationInterval() {
+ return null;
+ }
+ };
+ } else {
+ return config.getStateSet();
+ }
+ }
+
+ public void setOverdueConfig(final OverdueConfig config) {
+ this.config = config;
+ }
+
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/applicator/formatters/TestDefaultBillingStateFormatter.java b/overdue/src/test/java/org/killbill/billing/overdue/applicator/formatters/TestDefaultBillingStateFormatter.java
new file mode 100644
index 0000000..4894535
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/applicator/formatters/TestDefaultBillingStateFormatter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.applicator.formatters;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.overdue.OverdueTestSuiteNoDB;
+import org.killbill.billing.overdue.config.api.BillingState;
+
+public class TestDefaultBillingStateFormatter extends OverdueTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testBalanceFormatting() throws Exception {
+ final BillingState billingState = new BillingState(UUID.randomUUID(), 2, BigDecimal.TEN,
+ new LocalDate(), DateTimeZone.UTC, UUID.randomUUID(),
+ null, null);
+ final DefaultBillingStateFormatter formatter = new DefaultBillingStateFormatter(billingState);
+ Assert.assertEquals(formatter.getFormattedBalanceOfUnpaidInvoices(), "10.00");
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/applicator/OverdueBusListenerTester.java b/overdue/src/test/java/org/killbill/billing/overdue/applicator/OverdueBusListenerTester.java
new file mode 100644
index 0000000..4d3afcb
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/applicator/OverdueBusListenerTester.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.applicator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.events.OverdueChangeInternalEvent;
+
+import com.google.common.eventbus.Subscribe;
+
+public class OverdueBusListenerTester {
+
+ private static final Logger log = LoggerFactory.getLogger(OverdueBusListenerTester.class);
+
+ private final List<OverdueChangeInternalEvent> eventsReceived = new ArrayList<OverdueChangeInternalEvent>();
+
+ @Subscribe
+ public void handleOverdueChange(final OverdueChangeInternalEvent event) {
+
+ log.info("Received subscription transition.");
+ eventsReceived.add(event);
+ }
+
+ public List<OverdueChangeInternalEvent> getEventsReceived() {
+ return eventsReceived;
+ }
+
+ public void clearEventsReceived() {
+ eventsReceived.clear();
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java b/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java
new file mode 100644
index 0000000..247b843
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/applicator/TestOverdueStateApplicator.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.applicator;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.overdue.OverdueState;
+import org.killbill.billing.overdue.OverdueTestSuiteWithEmbeddedDB;
+import org.killbill.billing.overdue.config.OverdueConfig;
+import org.killbill.billing.overdue.config.api.OverdueStateSet;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+import org.killbill.billing.events.OverdueChangeInternalEvent;
+import org.killbill.billing.junction.DefaultBlockingState;
+
+import static com.jayway.awaitility.Awaitility.await;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+public class TestOverdueStateApplicator extends OverdueTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testApplicator() throws Exception {
+ final InputStream is = new ByteArrayInputStream(testOverdueHelper.getConfigXml().getBytes());
+ final OverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, OverdueConfig.class);
+ overdueWrapperFactory.setOverdueConfig(config);
+
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getId()).thenReturn(UUID.randomUUID());
+
+ final OverdueStateSet overdueStateSet = config.getStateSet();
+ final OverdueState clearState = config.getStateSet().findState(DefaultBlockingState.CLEAR_STATE_NAME);
+ OverdueState state;
+
+ state = config.getStateSet().findState("OD1");
+ applicator.apply(overdueStateSet, null, account, clearState, state, internalCallContext);
+ testOverdueHelper.checkStateApplied(state);
+ checkBussEvent("OD1");
+
+ state = config.getStateSet().findState("OD2");
+ applicator.apply(overdueStateSet, null, account, clearState, state, internalCallContext);
+ testOverdueHelper.checkStateApplied(state);
+ checkBussEvent("OD2");
+
+ state = config.getStateSet().findState("OD3");
+ applicator.apply(overdueStateSet, null, account, clearState, state, internalCallContext);
+ testOverdueHelper.checkStateApplied(state);
+ checkBussEvent("OD3");
+ }
+
+ private void checkBussEvent(final String state) throws Exception {
+ await().atMost(10, SECONDS).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ final List<OverdueChangeInternalEvent> events = listener.getEventsReceived();
+ return events.size() == 1;
+ }
+ });
+ final List<OverdueChangeInternalEvent> events = listener.getEventsReceived();
+ Assert.assertEquals(1, events.size());
+ Assert.assertEquals(state, events.get(0).getNextOverdueStateName());
+ listener.clearEventsReceived();
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java b/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java
new file mode 100644
index 0000000..4667992
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.calculator;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.UUID;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.overdue.OverdueTestSuiteNoDB;
+import org.killbill.billing.overdue.config.api.BillingState;
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+public class TestBillingStateCalculator extends OverdueTestSuiteNoDB {
+
+ protected LocalDate now;
+
+ @Override
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getTimeZone()).thenReturn(DateTimeZone.UTC);
+ Mockito.when(accountApi.getAccountById(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(account);
+ }
+
+ public BillingStateCalculator createBSCalc() {
+ now = new LocalDate();
+ final Collection<Invoice> invoices = new ArrayList<Invoice>();
+ invoices.add(createInvoice(now, BigDecimal.ZERO, null));
+ invoices.add(createInvoice(now.plusDays(1), BigDecimal.TEN, null));
+ invoices.add(createInvoice(now.plusDays(2), new BigDecimal("100.0"), null));
+
+ Mockito.when(invoiceApi.getUnpaidInvoicesByAccountId(Mockito.<UUID>any(), Mockito.<LocalDate>any(), Mockito.<InternalTenantContext>any())).thenReturn(invoices);
+
+ return new BillingStateCalculator(invoiceApi, clock) {
+ @Override
+ public BillingState calculateBillingState(final Account overdueable,
+ final InternalTenantContext context) {
+ return null;
+ }
+ };
+ }
+
+ public Invoice createInvoice(final LocalDate date, final BigDecimal balance, final List<InvoiceItem> invoiceItems) {
+ final Invoice invoice = Mockito.mock(Invoice.class);
+ Mockito.when(invoice.getBalance()).thenReturn(balance);
+ Mockito.when(invoice.getInvoiceDate()).thenReturn(date);
+ Mockito.when(invoice.getInvoiceItems()).thenReturn(invoiceItems);
+ Mockito.when(invoice.getId()).thenReturn(UUID.randomUUID());
+
+ return invoice;
+ }
+
+ @Test(groups = "fast")
+ public void testUnpaidInvoices() {
+ final BillingStateCalculator calc = createBSCalc();
+ final SortedSet<Invoice> invoices = calc.unpaidInvoicesForAccount(new UUID(0L, 0L), DateTimeZone.UTC, internalCallContext);
+
+ Assert.assertEquals(invoices.size(), 3);
+ Assert.assertEquals(BigDecimal.ZERO.compareTo(invoices.first().getBalance()), 0);
+ Assert.assertEquals(new BigDecimal("100.0").compareTo(invoices.last().getBalance()), 0);
+ }
+
+ @Test(groups = "fast")
+ public void testSum() {
+ final BillingStateCalculator calc = createBSCalc();
+ final SortedSet<Invoice> invoices = calc.unpaidInvoicesForAccount(new UUID(0L, 0L), DateTimeZone.UTC, internalCallContext);
+ Assert.assertEquals(new BigDecimal("110.0").compareTo(calc.sumBalance(invoices)), 0);
+ }
+
+ @Test(groups = "fast")
+ public void testEarliest() {
+ final BillingStateCalculator calc = createBSCalc();
+ final SortedSet<Invoice> invoices = calc.unpaidInvoicesForAccount(new UUID(0L, 0L), DateTimeZone.UTC, internalCallContext);
+ Assert.assertEquals(calc.earliest(invoices).getInvoiceDate(), now);
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/config/io/TestReadConfig.java b/overdue/src/test/java/org/killbill/billing/overdue/config/io/TestReadConfig.java
new file mode 100644
index 0000000..b61db01
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/config/io/TestReadConfig.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config.io;
+
+import org.testng.annotations.Test;
+
+import org.killbill.billing.overdue.OverdueTestSuiteNoDB;
+import org.killbill.billing.overdue.config.OverdueConfig;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+
+import com.google.common.io.Resources;
+
+public class TestReadConfig extends OverdueTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testConfigLoad() throws Exception {
+ XMLLoader.getObjectFromString(Resources.getResource("OverdueConfig.xml").toExternalForm(), OverdueConfig.class);
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/config/MockOverdueRules.java b/overdue/src/test/java/org/killbill/billing/overdue/config/MockOverdueRules.java
new file mode 100644
index 0000000..9c4ea10
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/config/MockOverdueRules.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+public class MockOverdueRules extends OverdueConfig {
+
+ public static final String CLEAR_STATE = "Clear";
+
+ @SuppressWarnings("unchecked")
+ public MockOverdueRules() {
+ final OverdueStatesAccount bundleODS = new OverdueStatesAccount();
+ bundleODS.setAccountOverdueStates(new DefaultOverdueState[]{new DefaultOverdueState().setName(CLEAR_STATE)});
+ setOverdueStates(bundleODS);
+
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/config/MockOverdueState.java b/overdue/src/test/java/org/killbill/billing/overdue/config/MockOverdueState.java
new file mode 100644
index 0000000..7aebb52
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/config/MockOverdueState.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+import org.killbill.billing.entitlement.api.Blockable;
+
+public class MockOverdueState<T extends Blockable> extends DefaultOverdueState {
+
+ public MockOverdueState() {
+ setName(MockOverdueRules.CLEAR_STATE);
+ }
+
+ public MockOverdueState(final String name, final boolean blockChanges, final boolean disableEntitlementAndBlockChanges) {
+ setName(name);
+ setBlockChanges(blockChanges);
+ setDisableEntitlement(disableEntitlementAndBlockChanges);
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/config/TestCondition.java b/overdue/src/test/java/org/killbill/billing/overdue/config/TestCondition.java
new file mode 100644
index 0000000..c34f30e
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/config/TestCondition.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.overdue.OverdueTestSuiteNoDB;
+import org.killbill.billing.overdue.config.api.BillingState;
+import org.killbill.billing.overdue.config.api.PaymentResponse;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.DefaultControlTag;
+import org.killbill.billing.util.tag.DescriptiveTag;
+import org.killbill.billing.util.tag.Tag;
+
+public class TestCondition extends OverdueTestSuiteNoDB {
+
+ @XmlRootElement(name = "condition")
+ private static class MockCondition extends DefaultCondition {}
+
+ @Test(groups = "fast")
+ public void testNumberOfUnpaidInvoicesEqualsOrExceeds() throws Exception {
+ final String xml =
+ "<condition>" +
+ " <numberOfUnpaidInvoicesEqualsOrExceeds>1</numberOfUnpaidInvoicesEqualsOrExceeds>" +
+ "</condition>";
+ final InputStream is = new ByteArrayInputStream(xml.getBytes());
+ final MockCondition c = XMLLoader.getObjectFromStreamNoValidation(is, MockCondition.class);
+ final UUID unpaidInvoiceId = UUID.randomUUID();
+
+ final BillingState state0 = new BillingState(new UUID(0L, 1L), 0, BigDecimal.ZERO, new LocalDate(),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS, new Tag[]{});
+ final BillingState state1 = new BillingState(new UUID(0L, 1L), 1, BigDecimal.ZERO, new LocalDate(),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS, new Tag[]{});
+ final BillingState state2 = new BillingState(new UUID(0L, 1L), 2, BigDecimal.ZERO, new LocalDate(),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS, new Tag[]{});
+
+ Assert.assertTrue(!c.evaluate(state0, new LocalDate()));
+ Assert.assertTrue(c.evaluate(state1, new LocalDate()));
+ Assert.assertTrue(c.evaluate(state2, new LocalDate()));
+ }
+
+ @Test(groups = "fast")
+ public void testTotalUnpaidInvoiceBalanceEqualsOrExceeds() throws Exception {
+ final String xml =
+ "<condition>" +
+ " <totalUnpaidInvoiceBalanceEqualsOrExceeds>100.00</totalUnpaidInvoiceBalanceEqualsOrExceeds>" +
+ "</condition>";
+ final InputStream is = new ByteArrayInputStream(xml.getBytes());
+ final MockCondition c = XMLLoader.getObjectFromStreamNoValidation(is, MockCondition.class);
+ final UUID unpaidInvoiceId = UUID.randomUUID();
+
+ final BillingState state0 = new BillingState(new UUID(0L, 1L), 0, BigDecimal.ZERO, new LocalDate(),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS, new Tag[]{});
+ final BillingState state1 = new BillingState(new UUID(0L, 1L), 1, new BigDecimal("100.00"), new LocalDate(),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS, new Tag[]{});
+ final BillingState state2 = new BillingState(new UUID(0L, 1L), 1, new BigDecimal("200.00"), new LocalDate(),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS, new Tag[]{});
+
+ Assert.assertTrue(!c.evaluate(state0, new LocalDate()));
+ Assert.assertTrue(c.evaluate(state1, new LocalDate()));
+ Assert.assertTrue(c.evaluate(state2, new LocalDate()));
+ }
+
+ @Test(groups = "fast")
+ public void testTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds() throws Exception {
+ final String xml =
+ "<condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds><unit>DAYS</unit><number>10</number></timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ "</condition>";
+ final InputStream is = new ByteArrayInputStream(xml.getBytes());
+ final MockCondition c = XMLLoader.getObjectFromStreamNoValidation(is, MockCondition.class);
+ final UUID unpaidInvoiceId = UUID.randomUUID();
+
+ final LocalDate now = new LocalDate();
+
+ final BillingState state0 = new BillingState(new UUID(0L, 1L), 0, BigDecimal.ZERO, null,
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS, new Tag[]{});
+ final BillingState state1 = new BillingState(new UUID(0L, 1L), 1, new BigDecimal("100.00"), now.minusDays(10),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS, new Tag[]{});
+ final BillingState state2 = new BillingState(new UUID(0L, 1L), 1, new BigDecimal("200.00"), now.minusDays(20),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS, new Tag[]{});
+
+ Assert.assertTrue(!c.evaluate(state0, now));
+ Assert.assertTrue(c.evaluate(state1, now));
+ Assert.assertTrue(c.evaluate(state2, now));
+ }
+
+ @Test(groups = "fast")
+ public void testResponseForLastFailedPaymentIn() throws Exception {
+ final String xml =
+ "<condition>" +
+ " <responseForLastFailedPaymentIn><response>INSUFFICIENT_FUNDS</response><response>DO_NOT_HONOR</response></responseForLastFailedPaymentIn>" +
+ "</condition>";
+ final InputStream is = new ByteArrayInputStream(xml.getBytes());
+ final MockCondition c = XMLLoader.getObjectFromStreamNoValidation(is, MockCondition.class);
+ final UUID unpaidInvoiceId = UUID.randomUUID();
+
+ final LocalDate now = new LocalDate();
+
+ final BillingState state0 = new BillingState(new UUID(0L, 1L), 0, BigDecimal.ZERO, null,
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.LOST_OR_STOLEN_CARD, new Tag[]{});
+ final BillingState state1 = new BillingState(new UUID(0L, 1L), 1, new BigDecimal("100.00"), now.minusDays(10),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS, new Tag[]{});
+ final BillingState state2 = new BillingState(new UUID(0L, 1L), 1, new BigDecimal("200.00"), now.minusDays(20),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.DO_NOT_HONOR, new Tag[]{});
+
+ Assert.assertTrue(!c.evaluate(state0, now));
+ Assert.assertTrue(c.evaluate(state1, now));
+ Assert.assertTrue(c.evaluate(state2, now));
+ }
+
+ @Test(groups = "fast")
+ public void testHasControlTag() throws Exception {
+ final String xml =
+ "<condition>" +
+ " <controlTag>OVERDUE_ENFORCEMENT_OFF</controlTag>" +
+ "</condition>";
+ final InputStream is = new ByteArrayInputStream(xml.getBytes());
+ final MockCondition c = XMLLoader.getObjectFromStreamNoValidation(is, MockCondition.class);
+ final UUID unpaidInvoiceId = UUID.randomUUID();
+
+ final LocalDate now = new LocalDate();
+
+ final ObjectType objectType = ObjectType.BUNDLE;
+
+ final UUID objectId = new UUID(0L, 1L);
+ final BillingState state0 = new BillingState(objectId, 0, BigDecimal.ZERO, null,
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.LOST_OR_STOLEN_CARD,
+ new Tag[]{new DefaultControlTag(ControlTagType.AUTO_INVOICING_OFF, objectType, objectId, clock.getUTCNow()),
+ new DescriptiveTag(UUID.randomUUID(), objectType, objectId, clock.getUTCNow())});
+
+ final BillingState state1 = new BillingState(objectId, 1, new BigDecimal("100.00"), now.minusDays(10),
+ DateTimeZone.UTC, unpaidInvoiceId, PaymentResponse.INSUFFICIENT_FUNDS,
+ new Tag[]{new DefaultControlTag(ControlTagType.OVERDUE_ENFORCEMENT_OFF, objectType, objectId, clock.getUTCNow())});
+
+ final BillingState state2 = new BillingState(objectId, 1, new BigDecimal("200.00"), now.minusDays(20),
+ DateTimeZone.UTC, unpaidInvoiceId,
+ PaymentResponse.DO_NOT_HONOR,
+ new Tag[]{new DefaultControlTag(ControlTagType.OVERDUE_ENFORCEMENT_OFF, objectType, objectId, clock.getUTCNow()),
+ new DefaultControlTag(ControlTagType.AUTO_INVOICING_OFF, objectType, objectId, clock.getUTCNow()),
+ new DescriptiveTag(UUID.randomUUID(), objectType, objectId, clock.getUTCNow())});
+
+ Assert.assertTrue(!c.evaluate(state0, now));
+ Assert.assertTrue(c.evaluate(state1, now));
+ Assert.assertTrue(c.evaluate(state2, now));
+ }
+}
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
new file mode 100644
index 0000000..533d905
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/config/TestOverdueConfig.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.config;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.billing.overdue.EmailNotification;
+import org.killbill.billing.overdue.OverdueTestSuiteNoDB;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+
+public class TestOverdueConfig extends OverdueTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testParseConfig() throws Exception {
+ final String xml = "<overdueConfig>" +
+ " <accountOverdueStates>" +
+ " <initialReevaluationInterval>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </initialReevaluationInterval>" +
+ " <state name=\"OD1\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>MONTHS</unit><number>1</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD1</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
+ " <autoReevaluationInterval>" +
+ " <unit>DAYS</unit><number>15</number>" +
+ " </autoReevaluationInterval>" +
+ " </state>" +
+ " <state name=\"OD2\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>MONTHS</unit><number>2</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD1</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>" +
+ " <autoReevaluationInterval>" +
+ " <unit>DAYS</unit><number>15</number>" +
+ " </autoReevaluationInterval>" +
+ " <enterStateEmailNotification>" +
+ " <subject>ToTo</subject><templateName>Titi</templateName>" +
+ " </enterStateEmailNotification>" +
+ " </state>" +
+ " </accountOverdueStates>" +
+ "</overdueConfig>";
+ final InputStream is = new ByteArrayInputStream(xml.getBytes());
+ final OverdueConfig c = XMLLoader.getObjectFromStreamNoValidation(is, OverdueConfig.class);
+ Assert.assertEquals(c.getStateSet().size(), 2);
+
+ Assert.assertNull(c.getStateSet().getStates()[0].getEnterStateEmailNotification());
+
+ Assert.assertNotNull(c.getStateSet().getInitialReevaluationInterval());
+ Assert.assertEquals(c.getStateSet().getInitialReevaluationInterval().getDays(), 1);
+
+ final EmailNotification secondNotification = c.getStateSet().getStates()[1].getEnterStateEmailNotification();
+ Assert.assertEquals(secondNotification.getSubject(), "ToTo");
+ Assert.assertEquals(secondNotification.getTemplateName(), "Titi");
+ Assert.assertFalse(secondNotification.isHTML());
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/glue/ApplicatorMockJunctionModule.java b/overdue/src/test/java/org/killbill/billing/overdue/glue/ApplicatorMockJunctionModule.java
new file mode 100644
index 0000000..45cd9b0
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/glue/ApplicatorMockJunctionModule.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.glue;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
+import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.junction.DefaultBlockingState;
+
+import com.google.inject.AbstractModule;
+
+public class ApplicatorMockJunctionModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ installBlockingApi();
+ }
+
+ public static class ApplicatorBlockingApi implements BlockingInternalApi {
+
+ private BlockingState blockingState;
+
+ public BlockingState getBlockingState() {
+ return blockingState;
+ }
+
+ @Override
+ public BlockingState getBlockingStateForService(final UUID blockableId, final BlockingStateType blockingStateType, final String serviceName, final InternalTenantContext context) {
+ if (blockingState != null && blockingState.getBlockedId().equals(blockableId)) {
+ return blockingState;
+ } else {
+ return DefaultBlockingState.getClearState(blockingStateType, serviceName, new ClockMock());
+ }
+ }
+
+ @Override
+ public List<BlockingState> getBlockingAllForAccount(final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setBlockingState(final BlockingState state, final InternalCallContext context) {
+ blockingState = state;
+ }
+ }
+
+ public void installBlockingApi() {
+ bind(BlockingInternalApi.class).toInstance(new ApplicatorBlockingApi());
+ }
+}
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
new file mode 100644
index 0000000..7b41126
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.mock.glue.MockAccountModule;
+import org.killbill.billing.mock.glue.MockEntitlementModule;
+import org.killbill.billing.mock.glue.MockSubscriptionModule;
+import org.killbill.billing.mock.glue.MockInvoiceModule;
+import org.killbill.billing.mock.glue.MockTagModule;
+import org.killbill.billing.overdue.TestOverdueHelper;
+import org.killbill.billing.overdue.applicator.OverdueBusListenerTester;
+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;
+import org.killbill.billing.util.glue.CallContextModule;
+import org.killbill.billing.util.glue.CustomFieldModule;
+
+public class TestOverdueModule extends DefaultOverdueModule {
+
+ public TestOverdueModule(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+
+ install(new AuditModule());
+ install(new CacheModule(configSource));
+ install(new CallContextModule());
+ install(new CustomFieldModule());
+ install(new EmailModule(configSource));
+ install(new MockAccountModule());
+ install(new MockEntitlementModule());
+ install(new MockInvoiceModule());
+ install(new MockTagModule());
+ install(new TemplateModule());
+
+ // We can't use the dumb mocks in MockJunctionModule here
+ install(new ApplicatorMockJunctionModule());
+
+ bind(OverdueBusListenerTester.class).asEagerSingleton();
+ bind(TestOverdueHelper.class).asEagerSingleton();
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModuleNoDB.java b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModuleNoDB.java
new file mode 100644
index 0000000..4fb7678
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModuleNoDB.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+import org.killbill.billing.mock.glue.MockNonEntityDaoModule;
+import org.killbill.billing.mock.glue.MockNotificationQueueModule;
+import org.killbill.billing.util.bus.InMemoryBusModule;
+
+public class TestOverdueModuleNoDB extends TestOverdueModule {
+
+ public TestOverdueModuleNoDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ public void configure() {
+ super.configure();
+
+ install(new GuicyKillbillTestNoDBModule());
+ install(new MockNonEntityDaoModule());
+ install(new MockNotificationQueueModule(configSource));
+ install(new InMemoryBusModule(configSource));
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModuleWithEmbeddedDB.java b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModuleWithEmbeddedDB.java
new file mode 100644
index 0000000..1e719a9
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModuleWithEmbeddedDB.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
+import org.killbill.billing.util.glue.BusModule;
+import org.killbill.billing.util.glue.MetricsModule;
+import org.killbill.billing.util.glue.NonEntityDaoModule;
+import org.killbill.billing.util.glue.NotificationQueueModule;
+
+public class TestOverdueModuleWithEmbeddedDB extends TestOverdueModule {
+
+ public TestOverdueModuleWithEmbeddedDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ public void configure() {
+ super.configure();
+
+ install(new GuicyKillbillTestWithEmbeddedDBModule());
+ install(new NonEntityDaoModule());
+ install(new NotificationQueueModule(configSource));
+ install(new MetricsModule());
+ install(new BusModule(configSource));
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/notification/MockOverdueNotifier.java b/overdue/src/test/java/org/killbill/billing/overdue/notification/MockOverdueNotifier.java
new file mode 100644
index 0000000..b059f23
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/notification/MockOverdueNotifier.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.notificationq.api.NotificationEvent;
+
+public class MockOverdueNotifier implements OverdueNotifier {
+
+ @Override
+ public void initialize() {
+ // do nothing
+ }
+
+ @Override
+ public void start() {
+ // do nothing
+ }
+
+ @Override
+ public void stop() {
+ // do nothing
+ }
+
+ @Override
+ public void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDate, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ }
+}
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
new file mode 100644
index 0000000..d128df7
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestDefaultOverdueCheckPoster.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.notificationq.api.NotificationEventWithMetadata;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.billing.overdue.OverdueTestSuiteWithEmbeddedDB;
+import org.killbill.billing.overdue.notification.OverdueCheckNotificationKey;
+import org.killbill.billing.overdue.notification.OverdueCheckNotifier;
+import org.killbill.billing.overdue.notification.OverdueCheckPoster;
+import org.killbill.billing.overdue.service.DefaultOverdueService;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+public class TestDefaultOverdueCheckPoster extends OverdueTestSuiteWithEmbeddedDB {
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ private EntitySqlDaoTransactionalJdbiWrapper entitySqlDaoTransactionalJdbiWrapper;
+ private NotificationQueue overdueQueue;
+ private DateTime testReferenceTime;
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ entitySqlDaoTransactionalJdbiWrapper = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao);
+
+ overdueQueue = notificationQueueService.getNotificationQueue(DefaultOverdueService.OVERDUE_SERVICE_NAME,
+ OverdueCheckNotifier.OVERDUE_CHECK_NOTIFIER_QUEUE);
+ Assert.assertTrue(overdueQueue.isStarted());
+
+ testReferenceTime = clock.getUTCNow();
+ }
+
+ @Test(groups = "slow")
+ public void testShouldntInsertMultipleNotificationsPerOverdueable() throws Exception {
+ final UUID accountId = UUID.randomUUID();
+ final Account overdueable = Mockito.mock(Account.class);
+ Mockito.when(overdueable.getId()).thenReturn(accountId);
+
+ insertOverdueCheckAndVerifyQueueContent(overdueable, 10, 10);
+ insertOverdueCheckAndVerifyQueueContent(overdueable, 5, 5);
+ insertOverdueCheckAndVerifyQueueContent(overdueable, 15, 5);
+
+ // Verify the final content of the queue
+ Assert.assertEquals(overdueQueue.getFutureNotificationForSearchKey1(OverdueCheckNotificationKey.class, internalCallContext.getAccountRecordId()).size(), 1);
+ }
+
+ private void insertOverdueCheckAndVerifyQueueContent(final Account account, final int nbDaysInFuture, final int expectedNbDaysInFuture) throws IOException {
+ final DateTime futureNotificationTime = testReferenceTime.plusDays(nbDaysInFuture);
+
+ final OverdueCheckNotificationKey notificationKey = new OverdueCheckNotificationKey(account.getId());
+ checkPoster.insertOverdueNotification(account.getId(), futureNotificationTime, OverdueCheckNotifier.OVERDUE_CHECK_NOTIFIER_QUEUE, notificationKey, internalCallContext);
+
+ final Collection<NotificationEventWithMetadata<OverdueCheckNotificationKey>> notificationsForKey = getNotificationsForOverdueable(account);
+ Assert.assertEquals(notificationsForKey.size(), 1);
+ final NotificationEventWithMetadata nm = notificationsForKey.iterator().next();
+ Assert.assertEquals(nm.getEvent(), notificationKey);
+ Assert.assertEquals(nm.getEffectiveDate(), testReferenceTime.plusDays(expectedNbDaysInFuture));
+ }
+
+ private Collection<NotificationEventWithMetadata<OverdueCheckNotificationKey>> getNotificationsForOverdueable(final Account account) {
+ return entitySqlDaoTransactionalJdbiWrapper.execute(new EntitySqlDaoTransactionWrapper<Collection<NotificationEventWithMetadata<OverdueCheckNotificationKey>>>() {
+ @Override
+ public Collection<NotificationEventWithMetadata<OverdueCheckNotificationKey>> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return ((OverdueCheckPoster)checkPoster).getFutureNotificationsForAccountInTransaction(entitySqlDaoWrapperFactory, overdueQueue, account.getId(), OverdueCheckNotificationKey.class, internalCallContext);
+ }
+ });
+ }
+}
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
new file mode 100644
index 0000000..b9da2b6
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.UUID;
+import java.util.concurrent.Callable;
+
+import org.joda.time.DateTime;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.overdue.OverdueTestSuiteWithEmbeddedDB;
+import org.killbill.billing.overdue.listener.OverdueDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+import static com.jayway.awaitility.Awaitility.await;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+public class TestOverdueCheckNotifier extends OverdueTestSuiteWithEmbeddedDB {
+
+ private OverdueDispatcherMock mockDispatcher;
+ private OverdueNotifier notifierForMock;
+
+
+
+ private static final class OverdueDispatcherMock extends OverdueDispatcher {
+
+ int eventCount = 0;
+ UUID latestAccountId = null;
+
+ public OverdueDispatcherMock(final InternalCallContextFactory internalCallContextFactory) {
+ super(null);
+ }
+
+ @Override
+ public void processOverdueForAccount(final UUID accountId, final InternalCallContext context) {
+ eventCount++;
+ latestAccountId = accountId;
+ }
+
+ public int getEventCount() {
+ return eventCount;
+ }
+
+ public UUID getLatestAccountId() {
+ return latestAccountId;
+ }
+ }
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ //super.beforeMethod();
+ // We override the parent method on purpose, because we want to register a different OverdueCheckNotifier
+
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(accountApi.getAccountById(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(account);
+
+ mockDispatcher = new OverdueDispatcherMock(internalCallContextFactory);
+ notifierForMock = new OverdueCheckNotifier(notificationQueueService, overdueProperties, internalCallContextFactory, mockDispatcher);
+
+ notifierForMock.initialize();
+ notifierForMock.start();
+ }
+
+ @Override
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ notifierForMock.stop();
+ super.afterMethod();
+ }
+
+ @Test(groups = "slow")
+ public void test() throws Exception {
+ final UUID accountId = new UUID(0L, 1L);
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getId()).thenReturn(accountId);
+ final DateTime now = clock.getUTCNow();
+ final DateTime readyTime = now.plusMillis(2000);
+
+ final OverdueCheckNotificationKey notificationKey = new OverdueCheckNotificationKey(accountId);
+ checkPoster.insertOverdueNotification(accountId, readyTime, OverdueCheckNotifier.OVERDUE_CHECK_NOTIFIER_QUEUE, notificationKey, internalCallContext);
+
+ // Move time in the future after the notification effectiveDate
+ clock.setDeltaFromReality(3000);
+
+ await().atMost(5, SECONDS).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return mockDispatcher.getEventCount() == 1;
+ }
+ });
+
+ Assert.assertEquals(mockDispatcher.getEventCount(), 1);
+ Assert.assertEquals(mockDispatcher.getLatestAccountId(), accountId);
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueNotificationKeyJson.java b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueNotificationKeyJson.java
new file mode 100644
index 0000000..4154d02
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueNotificationKeyJson.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.notification;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+import static org.testng.Assert.assertEquals;
+
+public class TestOverdueNotificationKeyJson {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Test(groups = "fast")
+ public void testOverdueNotificationKeyJson() throws Exception {
+ final UUID uuid = UUID.randomUUID();
+ final OverdueCheckNotificationKey e = new OverdueCheckNotificationKey(uuid);
+
+ final String json = mapper.writeValueAsString(e);
+
+ final Class<?> claz = Class.forName(OverdueCheckNotificationKey.class.getName());
+ final Object obj = mapper.readValue(json, claz);
+ Assert.assertTrue(obj.equals(e));
+ }
+
+ @Test(groups = "fast")
+ public void testOverdueNotificationKeyJsonWithNoKey() throws Exception {
+ final String uuidString = "bab0fca4-c628-4997-8980-14d6c3a98c48";
+ final String json = "{\"uuidKey\":\"" + uuidString + "\"}";
+
+ final Class<?> claz = Class.forName(OverdueCheckNotificationKey.class.getName());
+ final OverdueCheckNotificationKey obj = (OverdueCheckNotificationKey) mapper.readValue(json, claz);
+ assertEquals(obj.getUuidKey().toString(), uuidString);
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteNoDB.java b/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteNoDB.java
new file mode 100644
index 0000000..96533ab
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteNoDB.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+import javax.inject.Named;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.overdue.notification.OverduePoster;
+import org.killbill.billing.overdue.calculator.BillingStateCalculator;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.overdue.notification.OverdueNotifier;
+import org.killbill.billing.overdue.applicator.OverdueBusListenerTester;
+import org.killbill.billing.overdue.applicator.OverdueStateApplicator;
+import org.killbill.billing.overdue.glue.DefaultOverdueModule;
+import org.killbill.billing.overdue.glue.TestOverdueModuleNoDB;
+import org.killbill.billing.overdue.service.DefaultOverdueService;
+import org.killbill.billing.overdue.wrapper.OverdueWrapperFactory;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public abstract class OverdueTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ @Inject
+ protected AccountInternalApi accountApi;
+ @Inject
+ protected BillingStateCalculator calculatorBundle;
+ @Inject
+ protected BlockingInternalApi blockingApi;
+ @Inject
+ protected BusService busService;
+ @Inject
+ protected DefaultOverdueService service;
+ @Inject
+ protected PersistentBus bus;
+ @Inject
+ protected InternalCallContextFactory internalCallContextFactory;
+ @Inject
+ protected InvoiceInternalApi invoiceApi;
+ @Inject
+ protected NotificationQueueService notificationQueueService;
+ @Inject
+ protected OverdueBusListenerTester listener;
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_CHECK_NAMED)
+ @Inject
+ protected OverdueNotifier checkNotifier;
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_ASYNC_BUS_NAMED)
+ @Inject
+ protected OverdueNotifier asyncNotifier;
+ @Inject
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_CHECK_NAMED)
+ protected OverduePoster checkPoster;
+ @Inject
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_ASYNC_BUS_NAMED)
+ protected OverduePoster asyncPoster;
+ @Inject
+ protected OverdueStateApplicator applicator;
+ @Inject
+ protected OverdueUserApi overdueApi;
+ @Inject
+ protected OverdueProperties overdueProperties;
+ @Inject
+ protected OverdueWrapperFactory overdueWrapperFactory;
+ @Inject
+ protected TestOverdueHelper testOverdueHelper;
+
+ @BeforeClass(groups = "fast")
+ protected void beforeClass() throws Exception {
+ final Injector injector = Guice.createInjector(new TestOverdueModuleNoDB(configSource));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ bus.start();
+ service.initialize();
+ service.start();
+ }
+
+ @AfterMethod(groups = "fast")
+ public void afterMethod() {
+ service.stop();
+ bus.stop();
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteWithEmbeddedDB.java b/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..c77b503
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+import javax.inject.Named;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.overdue.notification.OverduePoster;
+import org.killbill.billing.overdue.calculator.BillingStateCalculator;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.overdue.notification.OverdueNotifier;
+import org.killbill.billing.overdue.applicator.OverdueBusListenerTester;
+import org.killbill.billing.overdue.applicator.OverdueStateApplicator;
+import org.killbill.billing.overdue.glue.DefaultOverdueModule;
+import org.killbill.billing.overdue.glue.TestOverdueModuleWithEmbeddedDB;
+import org.killbill.billing.overdue.service.DefaultOverdueService;
+import org.killbill.billing.overdue.wrapper.OverdueWrapperFactory;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.junction.BlockingInternalApi;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public abstract class OverdueTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
+
+ @Inject
+ protected AccountInternalApi accountApi;
+ @Inject
+ protected BillingStateCalculator calculatorBundle;
+ @Inject
+ protected BlockingInternalApi blockingApi;
+ @Inject
+ protected BusService busService;
+ @Inject
+ protected DefaultOverdueService service;
+ @Inject
+ protected CacheControllerDispatcher cacheControllerDispatcher;
+ @Inject
+ protected PersistentBus bus;
+ @Inject
+ protected InternalCallContextFactory internalCallContextFactory;
+ @Inject
+ protected InvoiceInternalApi invoiceApi;
+ @Inject
+ protected NotificationQueueService notificationQueueService;
+ @Inject
+ protected OverdueBusListenerTester listener;
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_CHECK_NAMED)
+ @Inject
+ protected OverdueNotifier checkNotifier;
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_ASYNC_BUS_NAMED)
+ @Inject
+ protected OverdueNotifier asyncNotifier;
+ @Inject
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_CHECK_NAMED)
+ protected OverduePoster checkPoster;
+ @Inject
+ @Named(DefaultOverdueModule.OVERDUE_NOTIFIER_ASYNC_BUS_NAMED)
+ protected OverduePoster asyncPoster;
+ @Inject
+ protected OverdueStateApplicator applicator;
+ @Inject
+ protected OverdueUserApi overdueApi;
+ @Inject
+ protected OverdueProperties overdueProperties;
+ @Inject
+ protected OverdueWrapperFactory overdueWrapperFactory;
+ @Inject
+ protected NonEntityDao nonEntityDao;
+ @Inject
+ protected TestOverdueHelper testOverdueHelper;
+
+ @BeforeClass(groups = "slow")
+ protected void beforeClass() throws Exception {
+ final Injector injector = Guice.createInjector(new TestOverdueModuleWithEmbeddedDB(configSource));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ cacheControllerDispatcher.clearAll();
+ bus.start();
+ bus.register(listener);
+ service.initialize();
+ service.start();
+ }
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ service.stop();
+ bus.unregister(listener);
+ bus.stop();
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java b/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java
new file mode 100644
index 0000000..5f49aba
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.testng.Assert;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.overdue.glue.ApplicatorMockJunctionModule.ApplicatorBlockingApi;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.junction.BlockingInternalApi;
+
+import com.google.inject.Inject;
+
+public class TestOverdueHelper {
+
+ private final String configXml =
+ "<overdueConfig>" +
+ " <accountOverdueStates>" +
+ " <initialReevaluationInterval>" +
+ " <unit>DAYS</unit><number>100</number>" +
+ " </initialReevaluationInterval>" +
+ " <state name=\"OD3\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>50</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD3</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>" +
+ " <autoReevaluationInterval>" +
+ " <unit>DAYS</unit><number>5</number>" +
+ " </autoReevaluationInterval>" +
+ " </state>" +
+ " <state name=\"OD2\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>40</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD2</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>" +
+ " <autoReevaluationInterval>" +
+ " <unit>DAYS</unit><number>5</number>" +
+ " </autoReevaluationInterval>" +
+ " </state>" +
+ " <state name=\"OD1\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>30</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD1</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
+ " <autoReevaluationInterval>" +
+ " <unit>DAYS</unit><number>100</number>" + // this number is intentionally too high
+ " </autoReevaluationInterval>" +
+ " </state>" +
+ " </accountOverdueStates>" +
+ "</overdueConfig>";
+
+ private final AccountInternalApi accountInternalApi;
+ private final InvoiceInternalApi invoiceInternalApi;
+ private final BlockingInternalApi blockingInternalApi;
+
+ @Inject
+ public TestOverdueHelper(final AccountInternalApi accountInternalApi,
+ final InvoiceInternalApi invoiceInternalApi,
+ final BlockingInternalApi blockingInternalApi) {
+ this.accountInternalApi = accountInternalApi;
+ this.invoiceInternalApi = invoiceInternalApi;
+ this.blockingInternalApi = blockingInternalApi;
+ }
+
+ public void checkStateApplied(final OverdueState state) {
+ final BlockingState result = ((ApplicatorBlockingApi) blockingInternalApi).getBlockingState();
+ checkStateApplied(result, state);
+ }
+
+ public void checkStateApplied(final BlockingState result, final OverdueState state) {
+ Assert.assertEquals(result.getStateName(), state.getName());
+ Assert.assertEquals(result.isBlockChange(), state.blockChanges());
+ Assert.assertEquals(result.isBlockEntitlement(), state.disableEntitlementAndChangesBlocked());
+ Assert.assertEquals(result.isBlockBilling(), state.disableEntitlementAndChangesBlocked());
+ }
+
+ public Account createAccount(final LocalDate dateOfLastUnPaidInvoice) throws SubscriptionBaseApiException, AccountApiException {
+
+ final UUID accountId = UUID.randomUUID();
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getId()).thenReturn(accountId);
+ Mockito.when(account.getTimeZone()).thenReturn(DateTimeZone.UTC);
+ Mockito.when(accountInternalApi.getAccountById(Mockito.eq(account.getId()), Mockito.<InternalTenantContext>any())).thenReturn(account);
+
+ final Invoice invoice = Mockito.mock(Invoice.class);
+ Mockito.when(invoice.getInvoiceDate()).thenReturn(dateOfLastUnPaidInvoice);
+ Mockito.when(invoice.getBalance()).thenReturn(BigDecimal.TEN);
+ Mockito.when(invoice.getId()).thenReturn(UUID.randomUUID());
+
+ final InvoiceItem item = Mockito.mock(InvoiceItem.class);
+ final List<InvoiceItem> items = new ArrayList<InvoiceItem>();
+ items.add(item);
+
+ Mockito.when(invoice.getInvoiceItems()).thenReturn(items);
+
+ final List<Invoice> invoices = new ArrayList<Invoice>();
+ invoices.add(invoice);
+ Mockito.when(invoiceInternalApi.getUnpaidInvoicesByAccountId(Mockito.<UUID>any(), Mockito.<LocalDate>any(), Mockito.<InternalTenantContext>any())).thenReturn(invoices);
+
+ return account;
+ }
+
+ public String getConfigXml() {
+ return configXml;
+ }
+}
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java b/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java
new file mode 100644
index 0000000..ab34456
--- /dev/null
+++ b/overdue/src/test/java/org/killbill/billing/overdue/wrapper/TestOverdueWrapper.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.overdue.wrapper;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.overdue.OverdueState;
+import org.killbill.billing.overdue.OverdueTestSuiteWithEmbeddedDB;
+import org.killbill.billing.overdue.config.OverdueConfig;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+import org.killbill.billing.junction.DefaultBlockingState;
+
+public class TestOverdueWrapper extends OverdueTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testWrapperBasic() throws Exception {
+ final InputStream is = new ByteArrayInputStream(testOverdueHelper.getConfigXml().getBytes());
+ final OverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, OverdueConfig.class);
+ overdueWrapperFactory.setOverdueConfig(config);
+
+ Account account;
+ OverdueWrapper wrapper;
+ OverdueState state;
+
+ state = config.getStateSet().findState("OD1");
+ account = testOverdueHelper.createAccount(clock.getUTCToday().minusDays(31));
+ wrapper = overdueWrapperFactory.createOverdueWrapperFor(account);
+ wrapper.refresh(internalCallContext);
+ testOverdueHelper.checkStateApplied(state);
+
+ state = config.getStateSet().findState("OD2");
+ account = testOverdueHelper.createAccount(clock.getUTCToday().minusDays(41));
+ wrapper = overdueWrapperFactory.createOverdueWrapperFor(account);
+ wrapper.refresh(internalCallContext);
+ testOverdueHelper.checkStateApplied(state);
+
+ state = config.getStateSet().findState("OD3");
+ account = testOverdueHelper.createAccount(clock.getUTCToday().minusDays(51));
+ wrapper = overdueWrapperFactory.createOverdueWrapperFor(account);
+ wrapper.refresh(internalCallContext);
+ testOverdueHelper.checkStateApplied(state);
+ }
+
+ @Test(groups = "slow")
+ public void testWrapperNoConfig() throws Exception {
+ overdueWrapperFactory.setOverdueConfig(null);
+
+ final Account account;
+ final OverdueWrapper wrapper;
+ final OverdueState state;
+
+ final InputStream is = new ByteArrayInputStream(testOverdueHelper.getConfigXml().getBytes());
+ final OverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, OverdueConfig.class);
+ state = config.getStateSet().findState(DefaultBlockingState.CLEAR_STATE_NAME);
+ account = testOverdueHelper.createAccount(clock.getUTCToday().minusDays(31));
+ wrapper = overdueWrapperFactory.createOverdueWrapperFor(account);
+ final OverdueState result = wrapper.refresh(internalCallContext);
+
+ Assert.assertEquals(result.getName(), state.getName());
+ Assert.assertEquals(result.blockChanges(), state.blockChanges());
+ Assert.assertEquals(result.disableEntitlementAndChangesBlocked(), state.disableEntitlementAndChangesBlocked());
+ }
+}
diff --git a/overdue/src/test/resources/resource.properties b/overdue/src/test/resources/resource.properties
index a7e225e..23ed0f6 100644
--- a/overdue/src/test/resources/resource.properties
+++ b/overdue/src/test/resources/resource.properties
@@ -1,5 +1,5 @@
-killbill.catalog.uri=file:src/test/resources/catalogSample.xml
-killbill.billing.persistent.bus.main.claimed=1
+org.killbill.catalog.uri=file:src/test/resources/catalogSample.xml
+org.killbill.persistent.bus.main.claimed=1
user.timezone=UTC
payment/pom.xml 63(+21 -42)
diff --git a/payment/pom.xml b/payment/pom.xml
index 3bd84aa..42aae78 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-payment</artifactId>
@@ -47,96 +47,75 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-account</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-invoice</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-junction</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-clock</artifactId>
+ <groupId>org.kill-bill.billing.plugin</groupId>
+ <artifactId>killbill-plugin-api-payment</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
- <type>test-jar</type>
- <scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-clock</artifactId>
+ <type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.plugin</groupId>
- <artifactId>killbill-plugin-api-payment</artifactId>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
-
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
new file mode 100644
index 0000000..631d2df
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPayment.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
+import org.killbill.billing.payment.dao.PaymentModelDao;
+import org.killbill.billing.payment.dao.RefundModelDao;
+import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+
+public class DefaultPayment extends EntityBase implements Payment {
+
+ private final UUID accountId;
+ private final UUID invoiceId;
+ private final UUID paymentMethodId;
+ private final BigDecimal amount;
+ private final BigDecimal paidAmount;
+ private final Currency currency;
+ private final DateTime effectiveDate;
+ private final Integer paymentNumber;
+ private final PaymentStatus paymentStatus;
+ private final List<PaymentAttempt> attempts;
+ private final PaymentInfoPlugin paymentPluginInfo;
+
+ private DefaultPayment(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate, final UUID accountId, final UUID invoiceId,
+ final UUID paymentMethodId, final BigDecimal amount, final BigDecimal paidAmount, final Currency currency,
+ final DateTime effectiveDate, final Integer paymentNumber,
+ final PaymentStatus paymentStatus,
+ @Nullable final PaymentInfoPlugin paymentPluginInfo,
+ final List<PaymentAttempt> attempts) {
+ super(id, createdDate, updatedDate);
+ this.accountId = accountId;
+ this.invoiceId = invoiceId;
+ this.paymentMethodId = paymentMethodId;
+ this.amount = amount;
+ this.paidAmount = paidAmount;
+ this.currency = currency;
+ this.effectiveDate = effectiveDate;
+ this.paymentNumber = paymentNumber;
+ this.paymentStatus = paymentStatus;
+ this.attempts = attempts;
+ this.paymentPluginInfo = paymentPluginInfo;
+ }
+
+ public DefaultPayment(final PaymentModelDao src, @Nullable final PaymentInfoPlugin paymentPluginInfo, final List<PaymentAttemptModelDao> attempts, final List<RefundModelDao> refunds) {
+ this(src.getId(),
+ src.getCreatedDate(),
+ src.getUpdatedDate(),
+ src.getAccountId(),
+ src.getInvoiceId(),
+ src.getPaymentMethodId(),
+ src.getAmount(),
+ toPaidAmount(src.getPaymentStatus(), src.getAmount(), refunds),
+ src.getCurrency(),
+ src.getEffectiveDate(),
+ src.getPaymentNumber(),
+ src.getPaymentStatus(),
+ paymentPluginInfo,
+ toPaymentAttempts(attempts));
+ }
+
+ @Override
+ public Integer getPaymentNumber() {
+ return paymentNumber;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ @Override
+ public UUID getPaymentMethodId() {
+ return paymentMethodId;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public BigDecimal getPaidAmount() {
+ return paidAmount;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public PaymentStatus getPaymentStatus() {
+ return paymentStatus;
+ }
+
+ @Override
+ public PaymentInfoPlugin getPaymentInfoPlugin() {
+ return paymentPluginInfo;
+ }
+
+ @Override
+ public List<PaymentAttempt> getAttempts() {
+ return attempts;
+ }
+
+ private static BigDecimal toPaidAmount(final PaymentStatus paymentStatus, final BigDecimal amount, final Iterable<RefundModelDao> refunds) {
+ if (paymentStatus != PaymentStatus.SUCCESS) {
+ return BigDecimal.ZERO;
+ }
+
+ BigDecimal result = amount;
+ for (final RefundModelDao cur : refunds) {
+ if (cur.getRefundStatus() == RefundStatus.COMPLETED) {
+ result = result.subtract(cur.getAmount());
+ }
+ }
+ return result;
+ }
+
+ private static List<PaymentAttempt> toPaymentAttempts(final Collection<PaymentAttemptModelDao> attempts) {
+ if (attempts == null || attempts.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return new ArrayList<PaymentAttempt>(Collections2.transform(attempts, new Function<PaymentAttemptModelDao, PaymentAttempt>() {
+ @Override
+ public PaymentAttempt apply(final PaymentAttemptModelDao input) {
+ return new PaymentAttempt() {
+ @Override
+ public PaymentStatus getPaymentStatus() {
+ return input.getProcessingStatus();
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return input.getEffectiveDate();
+ }
+
+ @Override
+ public UUID getId() {
+ return input.getId();
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return input.getCreatedDate();
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return input.getUpdatedDate();
+ }
+
+ @Override
+ public String getGatewayErrorCode() {
+ return input.getGatewayErrorCode();
+ }
+
+ @Override
+ public String getGatewayErrorMsg() {
+ return input.getGatewayErrorMsg();
+ }
+ };
+ }
+ }));
+ }
+}
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
new file mode 100644
index 0000000..ef74dbf
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.payment.core.PaymentMethodProcessor;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.payment.core.RefundProcessor;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+
+public class DefaultPaymentApi implements PaymentApi {
+
+ private final PaymentMethodProcessor methodProcessor;
+ private final PaymentProcessor paymentProcessor;
+ private final RefundProcessor refundProcessor;
+ private final InternalCallContextFactory internalCallContextFactory;
+ private final Clock clock;
+
+ @Inject
+ public DefaultPaymentApi(final PaymentMethodProcessor methodProcessor,
+ final PaymentProcessor paymentProcessor,
+ final RefundProcessor refundProcessor,
+ final Clock clock,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.methodProcessor = methodProcessor;
+ this.paymentProcessor = paymentProcessor;
+ this.refundProcessor = refundProcessor;
+ this.clock = clock;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public Payment createPayment(final Account account, final UUID invoiceId,
+ final BigDecimal amount, final CallContext context) throws PaymentApiException {
+ return paymentProcessor.createPayment(account, invoiceId, amount,
+ internalCallContextFactory.createInternalCallContext(account.getId(), context), true, false);
+ }
+
+ @Override
+ public Payment createExternalPayment(final Account account, final UUID invoiceId, final BigDecimal amount, final CallContext context) throws PaymentApiException {
+ return paymentProcessor.createPayment(account, invoiceId, amount,
+ internalCallContextFactory.createInternalCallContext(account.getId(), context), true, true);
+ }
+
+ @Override
+ public void notifyPendingPaymentOfStateChanged(final Account account, final UUID paymentId, final boolean isSuccess, final CallContext context) throws PaymentApiException {
+ paymentProcessor.notifyPendingPaymentOfStateChanged(account, paymentId, isSuccess,
+ internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
+ public Payment retryPayment(final Account account, final UUID paymentId, final CallContext context) throws PaymentApiException {
+ final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), context);
+ paymentProcessor.retryPaymentFromApi(paymentId, internalCallContext);
+ return getPayment(paymentId, false, context);
+ }
+
+ @Override
+ public Pagination<Payment> getPayments(final Long offset, final Long limit, final TenantContext context) {
+ return paymentProcessor.getPayments(offset, limit, context, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Pagination<Payment> getPayments(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext) throws PaymentApiException {
+ return paymentProcessor.getPayments(offset, limit, pluginName, tenantContext, internalCallContextFactory.createInternalTenantContext(tenantContext));
+ }
+
+ @Override
+ public Payment getPayment(final UUID paymentId, final boolean withPluginInfo, final TenantContext context) throws PaymentApiException {
+ final Payment payment = paymentProcessor.getPayment(paymentId, withPluginInfo, internalCallContextFactory.createInternalTenantContext(context));
+ if (payment == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT, paymentId);
+ }
+ return payment;
+ }
+
+ @Override
+ public Pagination<Payment> searchPayments(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
+ return paymentProcessor.searchPayments(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Pagination<Payment> searchPayments(final String searchKey, final Long offset, final Long limit, final String pluginName, final TenantContext context) throws PaymentApiException {
+ return paymentProcessor.searchPayments(searchKey, offset, limit, pluginName, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Pagination<Refund> getRefunds(final Long offset, final Long limit, final TenantContext context) {
+ return refundProcessor.getRefunds(offset, limit, context, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Pagination<Refund> getRefunds(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext) throws PaymentApiException {
+ return refundProcessor.getRefunds(offset, limit, pluginName, tenantContext, internalCallContextFactory.createInternalTenantContext(tenantContext));
+ }
+
+ @Override
+ public Pagination<Refund> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
+ return refundProcessor.searchRefunds(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Pagination<Refund> searchRefunds(final String searchKey, final Long offset, final Long limit, final String pluginName, final TenantContext context) throws PaymentApiException {
+ return refundProcessor.searchRefunds(searchKey, offset, limit, pluginName, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public List<Payment> getInvoicePayments(final UUID invoiceId, final TenantContext context) {
+ return paymentProcessor.getInvoicePayments(invoiceId, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public List<Payment> getAccountPayments(final UUID accountId, final TenantContext context)
+ throws PaymentApiException {
+ return paymentProcessor.getAccountPayments(accountId, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Refund getRefund(final UUID refundId, final boolean withPluginInfo, final TenantContext context) throws PaymentApiException {
+ return refundProcessor.getRefund(refundId, withPluginInfo, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Refund createRefund(final Account account, final UUID paymentId, final BigDecimal refundAmount, final CallContext context) throws PaymentApiException {
+ if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_REFUND_AMOUNT_NEGATIVE_OR_NULL);
+ }
+ return refundProcessor.createRefund(account, paymentId, refundAmount, false, ImmutableMap.<UUID, BigDecimal>of(),
+ internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
+ public void notifyPendingRefundOfStateChanged(final Account account, final UUID refundId, final boolean isSuccess, final CallContext context) throws PaymentApiException {
+ refundProcessor.notifyPendingRefundOfStateChanged(account, refundId, isSuccess,
+ internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
+ public Refund createRefundWithAdjustment(final Account account, final UUID paymentId, final BigDecimal refundAmount, final CallContext context) throws PaymentApiException {
+ if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_REFUND_AMOUNT_NEGATIVE_OR_NULL);
+ }
+ return refundProcessor.createRefund(account, paymentId, refundAmount, true, ImmutableMap.<UUID, BigDecimal>of(),
+ internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
+ public Refund createRefundWithItemsAdjustments(final Account account, final UUID paymentId, final Set<UUID> invoiceItemIds, final CallContext context) throws PaymentApiException {
+ final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts = new HashMap<UUID, BigDecimal>();
+ for (final UUID invoiceItemId : invoiceItemIds) {
+ invoiceItemIdsWithAmounts.put(invoiceItemId, null);
+ }
+
+ return refundProcessor.createRefund(account, paymentId, null, true, invoiceItemIdsWithAmounts,
+ internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
+ public Refund createRefundWithItemsAdjustments(final Account account, final UUID paymentId, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final CallContext context) throws PaymentApiException {
+ return refundProcessor.createRefund(account, paymentId, null, true, invoiceItemIdsWithAmounts,
+ internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
+ public List<Refund> getAccountRefunds(final Account account, final TenantContext context)
+ throws PaymentApiException {
+ return refundProcessor.getAccountRefunds(account, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public List<Refund> getPaymentRefunds(final UUID paymentId, final TenantContext context)
+ throws PaymentApiException {
+ return refundProcessor.getPaymentRefunds(paymentId, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Set<String> getAvailablePlugins() {
+ return methodProcessor.getAvailablePlugins();
+ }
+
+ @Override
+ public UUID addPaymentMethod(final String pluginName, final Account account,
+ final boolean setDefault, final PaymentMethodPlugin paymentMethodInfo, final CallContext context)
+ throws PaymentApiException {
+ return methodProcessor.addPaymentMethod(pluginName, account, setDefault, paymentMethodInfo,
+ internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
+ public List<PaymentMethod> getPaymentMethods(final Account account, final boolean withPluginInfo, final TenantContext context)
+ throws PaymentApiException {
+ return methodProcessor.getPaymentMethods(account, withPluginInfo, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public PaymentMethod getPaymentMethodById(final UUID paymentMethodId, final boolean includedDeleted, final boolean withPluginInfo, final TenantContext context)
+ throws PaymentApiException {
+ return methodProcessor.getPaymentMethodById(paymentMethodId, includedDeleted, withPluginInfo, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final TenantContext context) {
+ return methodProcessor.getPaymentMethods(offset, limit, context, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final String pluginName, final TenantContext context) throws PaymentApiException {
+ return methodProcessor.getPaymentMethods(offset, limit, pluginName, context, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
+ return methodProcessor.searchPaymentMethods(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final String pluginName, final TenantContext context) throws PaymentApiException {
+ return methodProcessor.searchPaymentMethods(searchKey, offset, limit, pluginName, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public void deletedPaymentMethod(final Account account, final UUID paymentMethodId, final boolean deleteDefaultPaymentMethodWithAutoPayOff, final CallContext context)
+ throws PaymentApiException {
+ methodProcessor.deletedPaymentMethod(account, paymentMethodId, deleteDefaultPaymentMethodWithAutoPayOff, internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
+ public void setDefaultPaymentMethod(final Account account, final UUID paymentMethodId, final CallContext context)
+ throws PaymentApiException {
+ methodProcessor.setDefaultPaymentMethod(account, paymentMethodId, internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
+ public List<PaymentMethod> refreshPaymentMethods(final String pluginName, final Account account, final CallContext context)
+ throws PaymentApiException {
+ return methodProcessor.refreshPaymentMethods(pluginName, account, internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ }
+
+ @Override
+ public List<PaymentMethod> refreshPaymentMethods(final Account account, final CallContext context)
+ throws PaymentApiException {
+ final InternalCallContext callContext = internalCallContextFactory.createInternalCallContext(account.getId(), context);
+
+ final List<PaymentMethod> paymentMethods = new LinkedList<PaymentMethod>();
+ for (final String pluginName : methodProcessor.getAvailablePlugins()) {
+ paymentMethods.addAll(methodProcessor.refreshPaymentMethods(pluginName, account, callContext));
+ }
+
+ return paymentMethods;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentErrorEvent.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentErrorEvent.java
new file mode 100644
index 0000000..fc5c995
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentErrorEvent.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.util.UUID;
+
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.PaymentErrorInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultPaymentErrorEvent extends BusEventBase implements PaymentErrorInternalEvent {
+
+ private final String message;
+ private final UUID accountId;
+ private final UUID invoiceId;
+ private final UUID paymentId;
+
+ @JsonCreator
+ public DefaultPaymentErrorEvent(@JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("invoiceId") final UUID invoiceId,
+ @JsonProperty("paymentId") final UUID paymentId,
+ @JsonProperty("message") final String message,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.message = message;
+ this.accountId = accountId;
+ this.invoiceId = invoiceId;
+ this.paymentId = paymentId;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ public UUID getPaymentId() {
+ return paymentId;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.PAYMENT_ERROR;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DefaultPaymentErrorEvent)) {
+ return false;
+ }
+
+ final DefaultPaymentErrorEvent that = (DefaultPaymentErrorEvent) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (message != null ? !message.equals(that.message) : that.message != null) {
+ return false;
+ }
+ if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = message != null ? message.hashCode() : 0;
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (invoiceId != null ? invoiceId.hashCode() : 0);
+ result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentInfoEvent.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentInfoEvent.java
new file mode 100644
index 0000000..561d7b9
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentInfoEvent.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.PaymentInfoInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultPaymentInfoEvent extends BusEventBase implements PaymentInfoInternalEvent {
+
+ private final UUID accountId;
+ private final UUID invoiceId;
+ private final UUID paymentId;
+ private final BigDecimal amount;
+ private final Integer paymentNumber;
+ private final PaymentStatus status;
+ private final DateTime effectiveDate;
+
+ @JsonCreator
+ public DefaultPaymentInfoEvent(@JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("invoiceId") final UUID invoiceId,
+ @JsonProperty("paymentId") final UUID paymentId,
+ @JsonProperty("amount") final BigDecimal amount,
+ @JsonProperty("paymentNumber") final Integer paymentNumber,
+ @JsonProperty("status") final PaymentStatus status,
+ @JsonProperty("extFirstPaymentRefId") final String extFirstPaymentRefId /* TODO for backward compatibility only */,
+ @JsonProperty("extSecondPaymentRefId") final String extSecondPaymentRefId /* TODO for backward compatibility only */,
+ @JsonProperty("effectiveDate") final DateTime effectiveDate,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.accountId = accountId;
+ this.invoiceId = invoiceId;
+ this.paymentId = paymentId;
+ this.amount = amount;
+ this.paymentNumber = paymentNumber;
+ this.status = status;
+ this.effectiveDate = effectiveDate;
+ }
+
+ public DefaultPaymentInfoEvent(final UUID accountId, final UUID invoiceId,
+ final UUID paymentId, final BigDecimal amount, final Integer paymentNumber,
+ final PaymentStatus status,
+ final DateTime effectiveDate,
+ final Long searchKey1,
+ final Long searchKey2,
+ final UUID userToken) {
+ this(accountId, invoiceId, paymentId, amount, paymentNumber, status, null, null,
+ effectiveDate, searchKey1, searchKey2, userToken);
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.PAYMENT_INFO;
+ }
+
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public UUID getPaymentId() {
+ return paymentId;
+ }
+
+ @Override
+ public Integer getPaymentNumber() {
+ return paymentNumber;
+ }
+
+ @Override
+ public PaymentStatus getStatus() {
+ return status;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultPaymentInfoEvent");
+ sb.append("{accountId=").append(accountId);
+ sb.append(", invoiceId=").append(invoiceId);
+ sb.append(", paymentId=").append(paymentId);
+ sb.append(", amount=").append(amount);
+ sb.append(", paymentNumber=").append(paymentNumber);
+ sb.append(", status=").append(status);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((accountId == null) ? 0 : accountId.hashCode());
+ result = prime * result + ((amount == null) ? 0 : amount.hashCode());
+ result = prime * result
+ + ((effectiveDate == null) ? 0 : effectiveDate.hashCode());
+ result = prime * result
+ + ((invoiceId == null) ? 0 : invoiceId.hashCode());
+ result = prime * result
+ + ((paymentId == null) ? 0 : paymentId.hashCode());
+ result = prime * result
+ + ((paymentNumber == null) ? 0 : paymentNumber.hashCode());
+ result = prime * result + ((status == null) ? 0 : status.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final DefaultPaymentInfoEvent other = (DefaultPaymentInfoEvent) obj;
+ if (accountId == null) {
+ if (other.accountId != null) {
+ return false;
+ }
+ } else if (!accountId.equals(other.accountId)) {
+ return false;
+ }
+ if (amount == null) {
+ if (other.amount != null) {
+ return false;
+ }
+ } else if (amount.compareTo(other.amount) != 0) {
+ return false;
+ }
+ if (effectiveDate == null) {
+ if (other.effectiveDate != null) {
+ return false;
+ }
+ } else if (effectiveDate.compareTo(other.effectiveDate) != 0) {
+ return false;
+ }
+ if (invoiceId == null) {
+ if (other.invoiceId != null) {
+ return false;
+ }
+ } else if (!invoiceId.equals(other.invoiceId)) {
+ return false;
+ }
+ if (paymentId == null) {
+ if (other.paymentId != null) {
+ return false;
+ }
+ } else if (!paymentId.equals(other.paymentId)) {
+ return false;
+ }
+ if (paymentNumber == null) {
+ if (other.paymentNumber != null) {
+ return false;
+ }
+ } else if (!paymentNumber.equals(other.paymentNumber)) {
+ return false;
+ }
+ if (status != other.status) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentMethod.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentMethod.java
new file mode 100644
index 0000000..2cd8ce7
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentMethod.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.payment.dao.PaymentMethodModelDao;
+import org.killbill.billing.entity.EntityBase;
+
+public class DefaultPaymentMethod extends EntityBase implements PaymentMethod {
+
+ private final UUID accountId;
+ private final Boolean isActive;
+ private final String pluginName;
+ private final PaymentMethodPlugin pluginDetail;
+
+ public DefaultPaymentMethod(final UUID paymentMethodId, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate,
+ final UUID accountId, final Boolean isActive, final String pluginName, @Nullable final PaymentMethodPlugin pluginDetail) {
+ super(paymentMethodId, createdDate, updatedDate);
+ this.accountId = accountId;
+ this.isActive = isActive;
+ this.pluginName = pluginName;
+ this.pluginDetail = pluginDetail;
+ }
+
+ public DefaultPaymentMethod(final UUID accountId, final String pluginName, final PaymentMethodPlugin pluginDetail) {
+ this(UUID.randomUUID(), null, null, accountId, true, pluginName, pluginDetail);
+ }
+
+ public DefaultPaymentMethod(final UUID paymentMethodId, final UUID accountId, final String pluginName) {
+ this(paymentMethodId, null, null, accountId, true, pluginName, null);
+ }
+
+ public DefaultPaymentMethod(final PaymentMethodModelDao input, @Nullable final PaymentMethodPlugin pluginDetail) {
+ this(input.getId(), input.getCreatedDate(), input.getUpdatedDate(), input.getAccountId(), input.isActive(), input.getPluginName(), pluginDetail);
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public Boolean isActive() {
+ return isActive;
+ }
+
+ @Override
+ public String getPluginName() {
+ return pluginName;
+ }
+
+ @Override
+ public PaymentMethodPlugin getPluginDetail() {
+ return pluginDetail;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentPluginErrorEvent.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentPluginErrorEvent.java
new file mode 100644
index 0000000..8bc0220
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentPluginErrorEvent.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.PaymentPluginErrorInternalEvent;
+
+import java.util.UUID;
+
+public class DefaultPaymentPluginErrorEvent extends BusEventBase implements PaymentPluginErrorInternalEvent {
+
+ private final String message;
+ private final UUID accountId;
+ private final UUID invoiceId;
+ private final UUID paymentId;
+
+ @JsonCreator
+ public DefaultPaymentPluginErrorEvent(@JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("invoiceId") final UUID invoiceId,
+ @JsonProperty("paymentId") final UUID paymentId,
+ @JsonProperty("message") final String message,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.message = message;
+ this.accountId = accountId;
+ this.invoiceId = invoiceId;
+ this.paymentId = paymentId;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ public UUID getPaymentId() {
+ return paymentId;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.PAYMENT_PLUGIN_ERROR;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DefaultPaymentPluginErrorEvent)) {
+ return false;
+ }
+
+ final DefaultPaymentPluginErrorEvent that = (DefaultPaymentPluginErrorEvent) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (message != null ? !message.equals(that.message) : that.message != null) {
+ return false;
+ }
+ if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = message != null ? message.hashCode() : 0;
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (invoiceId != null ? invoiceId.hashCode() : 0);
+ result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultRefund.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultRefund.java
new file mode 100644
index 0000000..5d5276d
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultRefund.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.payment.dao.RefundModelDao;
+import org.killbill.billing.payment.plugin.api.RefundInfoPlugin;
+
+public class DefaultRefund extends EntityBase implements Refund {
+
+ private final UUID paymentId;
+ private final BigDecimal amount;
+ private final Currency currency;
+ private final boolean isAdjusted;
+ private final DateTime effectiveDate;
+ private final RefundStatus refundStatus;
+ private final RefundInfoPlugin refundInfoPlugin;
+
+ public DefaultRefund(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate,
+ final UUID paymentId, final BigDecimal amount,
+ final Currency currency, final boolean isAdjusted, final DateTime effectiveDate,
+ final RefundStatus refundStatus, final RefundInfoPlugin refundInfoPlugin) {
+ super(id, createdDate, updatedDate);
+ this.paymentId = paymentId;
+ this.amount = amount;
+ this.currency = currency;
+ this.isAdjusted = isAdjusted;
+ this.effectiveDate = effectiveDate;
+ this.refundStatus = refundStatus;
+ this.refundInfoPlugin = refundInfoPlugin;
+ }
+
+ public DefaultRefund(final RefundModelDao refundModelDao, @Nullable final RefundInfoPlugin refundInfoPlugin) {
+ this(refundModelDao.getId(), refundModelDao.getCreatedDate(), refundModelDao.getUpdatedDate(),
+ refundModelDao.getPaymentId(), refundModelDao.getAmount(), refundModelDao.getCurrency(),
+ refundModelDao.isAdjusted(), refundModelDao.getCreatedDate(), refundModelDao.getRefundStatus(), refundInfoPlugin);
+ }
+
+ public DefaultRefund(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate,
+ final UUID paymentId, final BigDecimal amount,
+ final Currency currency, final boolean isAdjusted, final DateTime effectiveDate, final RefundStatus refundStatus) {
+ this(id, createdDate, updatedDate, paymentId, amount, currency, isAdjusted, effectiveDate, refundStatus, null);
+ }
+
+ @Override
+ public UUID getPaymentId() {
+ return paymentId;
+ }
+
+ @Override
+ public BigDecimal getRefundAmount() {
+ return amount;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public boolean isAdjusted() {
+ return isAdjusted;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public RefundStatus getRefundStatus() {
+ return refundStatus;
+ }
+
+ @Override
+ public RefundInfoPlugin getRefundInfoPlugin() {
+ return refundInfoPlugin;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultRefund{");
+ sb.append("paymentId=").append(paymentId);
+ sb.append(", amount=").append(amount);
+ sb.append(", currency=").append(currency);
+ sb.append(", isAdjusted=").append(isAdjusted);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", refundStatus=").append(refundStatus);
+ sb.append(", refundInfoPlugin=").append(refundInfoPlugin);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final DefaultRefund that = (DefaultRefund) o;
+
+ if (isAdjusted != that.isAdjusted) {
+ return false;
+ }
+ if (amount != null ? amount.compareTo(that.amount) != 0 : that.amount != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (effectiveDate != null ? effectiveDate.compareTo(that.effectiveDate) != 0 : that.effectiveDate != null) {
+ return false;
+ }
+ if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
+ return false;
+ }
+ if (refundInfoPlugin != null ? !refundInfoPlugin.equals(that.refundInfoPlugin) : that.refundInfoPlugin != null) {
+ return false;
+ }
+ if (refundStatus != that.refundStatus) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (isAdjusted ? 1 : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (refundStatus != null ? refundStatus.hashCode() : 0);
+ result = 31 * result + (refundInfoPlugin != null ? refundInfoPlugin.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/svcs/DefaultPaymentInternalApi.java b/payment/src/main/java/org/killbill/billing/payment/api/svcs/DefaultPaymentInternalApi.java
new file mode 100644
index 0000000..3ac67f9
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/api/svcs/DefaultPaymentInternalApi.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api.svcs;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentInternalApi;
+import org.killbill.billing.payment.api.PaymentMethod;
+import org.killbill.billing.payment.core.PaymentMethodProcessor;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+public class DefaultPaymentInternalApi implements PaymentInternalApi {
+
+ private final PaymentProcessor paymentProcessor;
+ private final PaymentMethodProcessor methodProcessor;
+
+ @Inject
+ public DefaultPaymentInternalApi(final PaymentProcessor paymentProcessor, final PaymentMethodProcessor methodProcessor) {
+ this.paymentProcessor = paymentProcessor;
+ this.methodProcessor = methodProcessor;
+ }
+
+ @Override
+ public Payment getPayment(final UUID paymentId, final InternalTenantContext context) throws PaymentApiException {
+ final Payment payment = paymentProcessor.getPayment(paymentId, false, context);
+ if (payment == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT, paymentId);
+ }
+ return payment;
+ }
+
+ @Override
+ public PaymentMethod getPaymentMethodById(final UUID paymentMethodId, final boolean includedInactive, final InternalTenantContext context) throws PaymentApiException {
+ return methodProcessor.getPaymentMethodById(paymentMethodId, includedInactive, false, context);
+ }
+
+ @Override
+ public List<Payment> getAccountPayments(final UUID accountId, final InternalTenantContext context) throws PaymentApiException {
+ return paymentProcessor.getAccountPayments(accountId, context);
+ }
+
+ @Override
+ public List<PaymentMethod> getPaymentMethods(final Account account, final InternalTenantContext context) throws PaymentApiException {
+ return methodProcessor.getPaymentMethods(account, false, context);
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/bus/InvoiceHandler.java b/payment/src/main/java/org/killbill/billing/payment/bus/InvoiceHandler.java
new file mode 100644
index 0000000..545dd5a
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/bus/InvoiceHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.bus;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.events.InvoiceCreationInternalEvent;
+import org.killbill.billing.account.api.AccountInternalApi;
+
+import com.google.common.eventbus.Subscribe;
+import com.google.inject.Inject;
+
+public class InvoiceHandler {
+
+ private final PaymentProcessor paymentProcessor;
+ private final AccountInternalApi accountApi;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ private static final Logger log = LoggerFactory.getLogger(InvoiceHandler.class);
+
+ @Inject
+ public InvoiceHandler(final AccountInternalApi accountApi,
+ final PaymentProcessor paymentProcessor,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.accountApi = accountApi;
+ this.paymentProcessor = paymentProcessor;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Subscribe
+ public void processInvoiceEvent(final InvoiceCreationInternalEvent event) {
+
+ log.info("Received invoice creation notification for account {} and invoice {}",
+ event.getAccountId(), event.getInvoiceId());
+
+ final Account account;
+ try {
+ final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(event.getSearchKey2(), event.getSearchKey1(), "PaymentRequestProcessor", CallOrigin.INTERNAL, UserType.SYSTEM, event.getUserToken());
+ account = accountApi.getAccountById(event.getAccountId(), internalContext);
+ paymentProcessor.createPayment(account, event.getInvoiceId(), null, internalContext, false, false);
+ } catch (AccountApiException e) {
+ log.error("Failed to process invoice payment", e);
+ } catch (PaymentApiException e) {
+ // Log as error unless:
+ if (e.getCode() != ErrorCode.PAYMENT_NULL_INVOICE.getCode() /* Nothing left to be paid */ &&
+ e.getCode() != ErrorCode.PAYMENT_CREATE_PAYMENT.getCode() /* User payment error */) {
+ log.error("Failed to process invoice payment {}", e.toString());
+ }
+ }
+ }
+}
+
+
diff --git a/payment/src/main/java/org/killbill/billing/payment/bus/PaymentTagHandler.java b/payment/src/main/java/org/killbill/billing/payment/bus/PaymentTagHandler.java
new file mode 100644
index 0000000..6eab837
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/bus/PaymentTagHandler.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.bus;
+
+import java.util.UUID;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.callcontext.DefaultCallContext;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.events.ControlTagDeletionInternalEvent;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.util.tag.ControlTagType;
+
+import com.google.common.eventbus.Subscribe;
+import com.google.inject.Inject;
+
+public class PaymentTagHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(PaymentTagHandler.class);
+
+ private final Clock clock;
+ private final AccountInternalApi accountApi;
+ private final PaymentProcessor paymentProcessor;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public PaymentTagHandler(final Clock clock,
+ final AccountInternalApi accountApi,
+ final PaymentProcessor paymentProcessor,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.clock = clock;
+ this.accountApi = accountApi;
+ this.paymentProcessor = paymentProcessor;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Subscribe
+ public void process_AUTO_PAY_OFF_removal(final ControlTagDeletionInternalEvent event) {
+
+ if (event.getTagDefinition().getName().equals(ControlTagType.AUTO_PAY_OFF.toString()) && event.getObjectType() == ObjectType.ACCOUNT) {
+ final UUID accountId = event.getObjectId();
+ processUnpaid_AUTO_PAY_OFF_payments(accountId, event.getSearchKey1(), event.getSearchKey2(), event.getUserToken());
+ }
+ }
+
+ private void processUnpaid_AUTO_PAY_OFF_payments(final UUID accountId, final Long accountRecordId, final Long tenantRecordId, final UUID userToken) {
+ try {
+ final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId,
+ "PaymentRequestProcessor", CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
+ final Account account = accountApi.getAccountById(accountId, internalCallContext);
+ paymentProcessor.process_AUTO_PAY_OFF_removal(account, internalCallContext);
+
+ } catch (AccountApiException e) {
+ log.warn(String.format("Failed to process process removal AUTO_PAY_OFF for account %s", accountId), e);
+ } catch (PaymentApiException e) {
+ log.warn(String.format("Failed to process process removal AUTO_PAY_OFF for account %s", accountId), e);
+ }
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
new file mode 100644
index 0000000..81dba8a
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.core;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.bus.api.PersistentBus;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+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.PaymentMethodKVInfo;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.billing.payment.dao.PaymentMethodModelDao;
+import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.billing.payment.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.callcontext.TenantContext;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.EntityPaginationBuilder;
+import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED;
+import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPagination;
+import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationFromPlugins;
+
+public class PaymentMethodProcessor extends ProcessorBase {
+
+ private static final Logger log = LoggerFactory.getLogger(PaymentMethodProcessor.class);
+
+ @Inject
+ public PaymentMethodProcessor(final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry,
+ final AccountInternalApi accountInternalApi,
+ final InvoiceInternalApi invoiceApi,
+ final PersistentBus eventBus,
+ final PaymentDao paymentDao,
+ final NonEntityDao nonEntityDao,
+ final TagInternalApi tagUserApi,
+ final GlobalLocker locker,
+ @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor) {
+ super(pluginRegistry, accountInternalApi, eventBus, paymentDao, nonEntityDao, tagUserApi, locker, executor, invoiceApi);
+ }
+
+ public UUID addPaymentMethod(final String paymentPluginServiceName, final Account account,
+ final boolean setDefault, final PaymentMethodPlugin paymentMethodProps, final InternalCallContext context)
+ throws PaymentApiException {
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT);
+
+ return new WithAccountLock<UUID>().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback<UUID>() {
+
+ @Override
+ public UUID doOperation() throws PaymentApiException {
+ PaymentMethod pm = null;
+ PaymentPluginApi pluginApi = null;
+ try {
+ pluginApi = getPaymentPluginApi(paymentPluginServiceName);
+ pm = new DefaultPaymentMethod(account.getId(), paymentPluginServiceName, paymentMethodProps);
+ pluginApi.addPaymentMethod(account.getId(), pm.getId(), paymentMethodProps, setDefault, context.toCallContext(tenantId));
+ final PaymentMethodModelDao pmModel = new PaymentMethodModelDao(pm.getId(), pm.getCreatedDate(), pm.getUpdatedDate(),
+ pm.getAccountId(), pm.getPluginName(), pm.isActive());
+ paymentDao.insertPaymentMethod(pmModel, context);
+
+ if (setDefault) {
+ accountInternalApi.updatePaymentMethod(account.getId(), pm.getId(), context);
+ }
+ } catch (PaymentPluginApiException e) {
+ log.warn("Error adding payment method " + pm.getId() + " for plugin " + paymentPluginServiceName, e);
+ // STEPH all errors should also take a pluginName
+ throw new PaymentApiException(ErrorCode.PAYMENT_ADD_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
+ } catch (AccountApiException e) {
+ throw new PaymentApiException(e);
+ }
+ return pm.getId();
+ }
+ });
+ }
+
+ public List<PaymentMethod> getPaymentMethods(final Account account, final boolean withPluginInfo, final InternalTenantContext context) throws PaymentApiException {
+
+ final List<PaymentMethodModelDao> paymentMethodModels = paymentDao.getPaymentMethods(account.getId(), context);
+ if (paymentMethodModels.size() == 0) {
+ return Collections.emptyList();
+ }
+ return getPaymentMethodInternal(paymentMethodModels, withPluginInfo, context);
+ }
+
+ public PaymentMethod getPaymentMethodById(final UUID paymentMethodId, final boolean includedDeleted, final boolean withPluginInfo, final InternalTenantContext context)
+ throws PaymentApiException {
+ final PaymentMethodModelDao paymentMethodModel = includedDeleted ? paymentDao.getPaymentMethodIncludedDeleted(paymentMethodId, context) : paymentDao.getPaymentMethod(paymentMethodId, context);
+ if (paymentMethodModel == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
+ }
+
+ return buildDefaultPaymentMethod(paymentMethodModel, withPluginInfo, context);
+ }
+
+ private PaymentMethod buildDefaultPaymentMethod(final PaymentMethodModelDao paymentMethodModelDao, final boolean withPluginInfo, final InternalTenantContext context) throws PaymentApiException {
+ final PaymentMethodPlugin paymentMethodPlugin;
+ if (withPluginInfo) {
+ try {
+ final PaymentPluginApi pluginApi = getPaymentPluginApi(paymentMethodModelDao.getPluginName());
+ paymentMethodPlugin = pluginApi.getPaymentMethodDetail(paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId(), buildTenantContext(context));
+ } catch (PaymentPluginApiException e) {
+ log.warn("Error retrieving payment method " + paymentMethodModelDao.getId() + " from plugin " + paymentMethodModelDao.getPluginName(), e);
+ throw new PaymentApiException(ErrorCode.PAYMENT_GET_PAYMENT_METHODS, paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId());
+ }
+ } else {
+ paymentMethodPlugin = null;
+ }
+
+ return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
+ }
+
+ public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) {
+ return getEntityPaginationFromPlugins(getAvailablePlugins(),
+ offset,
+ limit,
+ new EntityPaginationBuilder<PaymentMethod, PaymentApiException>() {
+ @Override
+ public Pagination<PaymentMethod> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+ return getPaymentMethods(offset, limit, pluginName, tenantContext, internalTenantContext);
+ }
+ });
+ }
+
+ public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+ final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+
+ return getEntityPagination(limit,
+ new SourcePaginationBuilder<PaymentMethodModelDao, PaymentApiException>() {
+ @Override
+ public Pagination<PaymentMethodModelDao> build() {
+ // Find all payment methods for all accounts
+ return paymentDao.getPaymentMethods(pluginName, offset, limit, internalTenantContext);
+ }
+ },
+ new Function<PaymentMethodModelDao, PaymentMethod>() {
+ @Override
+ public PaymentMethod apply(final PaymentMethodModelDao paymentMethodModelDao) {
+ PaymentMethodPlugin paymentMethodPlugin = null;
+ try {
+ paymentMethodPlugin = pluginApi.getPaymentMethodDetail(paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId(), tenantContext);
+ } catch (final PaymentPluginApiException e) {
+ log.warn("Unable to find payment method id " + paymentMethodModelDao.getId() + " in plugin " + pluginName);
+ // We still want to return a payment method object, even though the plugin details are missing
+ }
+
+ return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
+ }
+ }
+ );
+ }
+
+ public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final InternalTenantContext internalTenantContext) {
+ return getEntityPaginationFromPlugins(getAvailablePlugins(),
+ offset,
+ limit,
+ new EntityPaginationBuilder<PaymentMethod, PaymentApiException>() {
+ @Override
+ public Pagination<PaymentMethod> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+ return searchPaymentMethods(searchKey, offset, limit, pluginName, internalTenantContext);
+ }
+ });
+ }
+
+ public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final String pluginName, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+ final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+
+ return getEntityPagination(limit,
+ new SourcePaginationBuilder<PaymentMethodPlugin, PaymentApiException>() {
+ @Override
+ public Pagination<PaymentMethodPlugin> build() throws PaymentApiException {
+ try {
+ return pluginApi.searchPaymentMethods(searchKey, offset, limit, buildTenantContext(internalTenantContext));
+ } catch (final PaymentPluginApiException e) {
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENT_METHODS, pluginName, searchKey);
+ }
+ }
+ },
+ new Function<PaymentMethodPlugin, PaymentMethod>() {
+ @Override
+ public PaymentMethod apply(final PaymentMethodPlugin paymentMethodPlugin) {
+ if (paymentMethodPlugin.getKbPaymentMethodId() == null) {
+ // Garbage from the plugin?
+ log.debug("Plugin {} returned a payment method without a kbPaymentMethodId for searchKey {}", pluginName, searchKey);
+ return null;
+ }
+
+ final PaymentMethodModelDao paymentMethodModelDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodPlugin.getKbPaymentMethodId(), internalTenantContext);
+ if (paymentMethodModelDao == null) {
+ log.warn("Unable to find payment method id " + paymentMethodPlugin.getKbPaymentMethodId() + " present in plugin " + pluginName);
+ return null;
+ }
+
+ return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
+ }
+ }
+ );
+ }
+
+ public PaymentMethod getExternalPaymentMethod(final Account account, final InternalTenantContext context) throws PaymentApiException {
+ final List<PaymentMethod> paymentMethods = getPaymentMethods(account, false, context);
+ for (final PaymentMethod paymentMethod : paymentMethods) {
+ if (ExternalPaymentProviderPlugin.PLUGIN_NAME.equals(paymentMethod.getPluginName())) {
+ return paymentMethod;
+ }
+ }
+
+ return null;
+ }
+
+ public ExternalPaymentProviderPlugin getExternalPaymentProviderPlugin(final Account account, final InternalCallContext context) throws PaymentApiException {
+ // Check if this account has already used the external payment plugin
+ // If not, it's the first time - add a payment method for it
+ if (getExternalPaymentMethod(account, context) == null) {
+ final DefaultNoOpPaymentMethodPlugin props = new DefaultNoOpPaymentMethodPlugin(UUID.randomUUID().toString(), false, ImmutableList.<PaymentMethodKVInfo>of());
+ addPaymentMethod(ExternalPaymentProviderPlugin.PLUGIN_NAME, account, false, props, context);
+ }
+
+ return (ExternalPaymentProviderPlugin) getPaymentPluginApi(ExternalPaymentProviderPlugin.PLUGIN_NAME);
+ }
+
+ private List<PaymentMethod> getPaymentMethodInternal(final List<PaymentMethodModelDao> paymentMethodModels, final boolean withPluginInfo, final InternalTenantContext context)
+ throws PaymentApiException {
+
+ final List<PaymentMethod> result = new ArrayList<PaymentMethod>(paymentMethodModels.size());
+ for (final PaymentMethodModelDao paymentMethodModel : paymentMethodModels) {
+ final PaymentMethod pm = buildDefaultPaymentMethod(paymentMethodModel, withPluginInfo, context);
+ result.add(pm);
+ }
+ return result;
+ }
+
+ public void deletedPaymentMethod(final Account account, final UUID paymentMethodId,
+ final boolean deleteDefaultPaymentMethodWithAutoPayOff, final InternalCallContext context)
+ throws PaymentApiException {
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT);
+
+ new WithAccountLock<Void>().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback<Void>() {
+
+ @Override
+ public Void doOperation() throws PaymentApiException {
+ final PaymentMethodModelDao paymentMethodModel = paymentDao.getPaymentMethod(paymentMethodId, context);
+ if (paymentMethodModel == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
+ }
+
+ try {
+ // Note: account.getPaymentMethodId() may be null
+ if (paymentMethodId.equals(account.getPaymentMethodId())) {
+ if (!deleteDefaultPaymentMethodWithAutoPayOff) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_DEL_DEFAULT_PAYMENT_METHOD, account.getId());
+ } else {
+ final boolean isAccountAutoPayOff = isAccountAutoPayOff(account.getId(), context);
+ if (!isAccountAutoPayOff) {
+ log.info("Setting account {} to AUTO_PAY_OFF because of default payment method deletion", account.getId());
+ setAccountAutoPayOff(account.getId(), context);
+ }
+ accountInternalApi.removePaymentMethod(account.getId(), context);
+ }
+ }
+ final PaymentPluginApi pluginApi = getPluginApi(paymentMethodId, context);
+ pluginApi.deletePaymentMethod(account.getId(), paymentMethodId, context.toCallContext(tenantId));
+ paymentDao.deletedPaymentMethod(paymentMethodId, context);
+ return null;
+ } catch (PaymentPluginApiException e) {
+ log.warn("Error deleting payment method " + paymentMethodId, e);
+ throw new PaymentApiException(ErrorCode.PAYMENT_DEL_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
+ } catch (AccountApiException e) {
+ throw new PaymentApiException(e);
+ }
+ }
+ });
+ }
+
+ public void setDefaultPaymentMethod(final Account account, final UUID paymentMethodId, final InternalCallContext context)
+ throws PaymentApiException {
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT);
+
+ new WithAccountLock<Void>().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback<Void>() {
+
+ @Override
+ public Void doOperation() throws PaymentApiException {
+ final PaymentMethodModelDao paymentMethodModel = paymentDao.getPaymentMethod(paymentMethodId, context);
+ if (paymentMethodModel == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
+ }
+
+ try {
+ final PaymentPluginApi pluginApi = getPluginApi(paymentMethodId, context);
+
+ pluginApi.setDefaultPaymentMethod(account.getId(), paymentMethodId, context.toCallContext(tenantId));
+ accountInternalApi.updatePaymentMethod(account.getId(), paymentMethodId, context);
+ return null;
+ } catch (PaymentPluginApiException e) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_UPD_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
+ } catch (AccountApiException e) {
+ throw new PaymentApiException(e);
+ }
+ }
+ });
+ }
+
+ private PaymentPluginApi getPluginApi(final UUID paymentMethodId, final InternalTenantContext context)
+ throws PaymentApiException {
+ final PaymentMethodModelDao paymentMethod = paymentDao.getPaymentMethod(paymentMethodId, context);
+ if (paymentMethod == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
+ }
+ return getPaymentPluginApi(paymentMethod.getPluginName());
+ }
+
+ /**
+ * This refreshed the payment methods from the plugin for cases when adding payment method does not flow through KB because of PCI compliance
+ * issues. The logic below is not optimal because there is no atomicity in the step but the good news is that this is idempotent so can always be
+ * replayed if necessary-- partial failure scenario.
+ *
+ * @param pluginName
+ * @param account
+ * @param context
+ * @return the list of payment methods -- should be identical between KB, the plugin view-- if it keeps a state-- and the gateway.
+ * @throws PaymentApiException
+ */
+ public List<PaymentMethod> refreshPaymentMethods(final String pluginName, final Account account, final InternalCallContext context) throws PaymentApiException {
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT);
+
+ // Don't hold the account lock while fetching the payment methods from the gateway as those could change anyway
+ final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+ final List<PaymentMethodInfoPlugin> pluginPms;
+ try {
+ pluginPms = pluginApi.getPaymentMethods(account.getId(), true, context.toCallContext(tenantId));
+ // The method should never return null by convention, but let's not trust the plugin...
+ if (pluginPms == null) {
+ log.debug("No payment methods defined on the account {} for plugin {}", account.getId(), pluginName);
+ return ImmutableList.<PaymentMethod>of();
+ }
+ } catch (PaymentPluginApiException e) {
+ log.warn("Error refreshing payment methods for account " + account.getId() + " and plugin " + pluginName, e);
+ throw new PaymentApiException(ErrorCode.PAYMENT_REFRESH_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
+ }
+
+ return new WithAccountLock<List<PaymentMethod>>().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback<List<PaymentMethod>>() {
+
+ @Override
+ public List<PaymentMethod> doOperation() throws PaymentApiException {
+
+ UUID defaultPaymentMethodId = null;
+
+ final List<PaymentMethodInfoPlugin> pluginPmsWithId = new ArrayList<PaymentMethodInfoPlugin>();
+ final List<PaymentMethodModelDao> finalPaymentMethods = new ArrayList<PaymentMethodModelDao>();
+ 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() : UUID.randomUUID();
+ final PaymentMethod input = new DefaultPaymentMethod(paymentMethodId, account.getId(), pluginName);
+ final PaymentMethodModelDao pmModel = new PaymentMethodModelDao(input.getId(), input.getCreatedDate(), input.getUpdatedDate(),
+ input.getAccountId(), input.getPluginName(), input.isActive());
+ finalPaymentMethods.add(pmModel);
+
+ pluginPmsWithId.add(new DefaultPaymentMethodInfoPlugin(cur, paymentMethodId));
+
+ // Note: we do not unset the default payment method in Kill Bill even if isDefault is false here.
+ // Some gateways don't support the concept of "default" payment methods, in that case the plugin
+ // will always return false - it's Kill Bill in that case which is responsible to manage default payment methods
+ if (cur.isDefault()) {
+ defaultPaymentMethodId = paymentMethodId;
+ }
+ }
+
+ final List<PaymentMethodModelDao> refreshedPaymentMethods = paymentDao.refreshPaymentMethods(account.getId(),
+ pluginName,
+ finalPaymentMethods,
+ context);
+ try {
+ pluginApi.resetPaymentMethods(account.getId(), pluginPmsWithId);
+ } catch (PaymentPluginApiException e) {
+ log.warn("Error resetting payment methods for account " + account.getId() + " and plugin " + pluginName, e);
+ throw new PaymentApiException(ErrorCode.PAYMENT_REFRESH_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
+ }
+
+ try {
+ updateDefaultPaymentMethodIfNeeded(pluginName, account, defaultPaymentMethodId, context);
+ } catch (AccountApiException e) {
+ throw new PaymentApiException(e);
+ }
+
+ return ImmutableList.<PaymentMethod>copyOf(Collections2.transform(refreshedPaymentMethods, new Function<PaymentMethodModelDao, PaymentMethod>() {
+ @Override
+ public PaymentMethod apply(final PaymentMethodModelDao input) {
+ return new DefaultPaymentMethod(input, null);
+ }
+ }));
+ }
+ });
+ }
+
+ private void updateDefaultPaymentMethodIfNeeded(final String pluginName, final Account account, @Nullable final UUID defaultPluginPaymentMethodId, final InternalCallContext context) throws PaymentApiException, AccountApiException {
+
+ // If the plugin does not have a default payment gateway, we keep the current default payment method in KB account as it is.
+ if (defaultPluginPaymentMethodId == null) {
+ return;
+ }
+
+ // Some gateways have the concept of default payment methods. Kill Bill has also its own default payment method
+ // and is authoritative on this matter. However, if the default payment method is associated with a given plugin,
+ // and if the default payment method in that plugin has changed, we will reflect this change in Kill Bill as well.
+
+ boolean shouldUpdateDefaultPaymentMethod = true;
+ if (account.getPaymentMethodId() != null) {
+ final PaymentMethodModelDao currentDefaultPaymentMethod = paymentDao.getPaymentMethod(account.getPaymentMethodId(), context);
+ shouldUpdateDefaultPaymentMethod = pluginName.equals(currentDefaultPaymentMethod.getPluginName());
+ }
+ if (shouldUpdateDefaultPaymentMethod) {
+ accountInternalApi.updatePaymentMethod(account.getId(), defaultPluginPaymentMethodId, context);
+ }
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java b/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
new file mode 100644
index 0000000..53ad47e
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.core;
+
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.bus.api.PersistentBus.EventBusException;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.commons.locker.GlobalLock;
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.commons.locker.LockFailedException;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.billing.payment.dao.PaymentMethodModelDao;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.globallocker.LockerType;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.Tag;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+
+public abstract class ProcessorBase {
+
+ private static final int NB_LOCK_TRY = 5;
+
+ protected final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry;
+ protected final AccountInternalApi accountInternalApi;
+ protected final PersistentBus eventBus;
+ protected final GlobalLocker locker;
+ protected final ExecutorService executor;
+ protected final PaymentDao paymentDao;
+ protected final NonEntityDao nonEntityDao;
+ protected final TagInternalApi tagInternalApi;
+
+ private static final Logger log = LoggerFactory.getLogger(ProcessorBase.class);
+ protected final InvoiceInternalApi invoiceApi;
+
+ public ProcessorBase(final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry,
+ final AccountInternalApi accountInternalApi,
+ final PersistentBus eventBus,
+ final PaymentDao paymentDao,
+ final NonEntityDao nonEntityDao,
+ final TagInternalApi tagInternalApi,
+ final GlobalLocker locker,
+ final ExecutorService executor, final InvoiceInternalApi invoiceApi) {
+ this.pluginRegistry = pluginRegistry;
+ this.accountInternalApi = accountInternalApi;
+ this.eventBus = eventBus;
+ this.paymentDao = paymentDao;
+ this.nonEntityDao = nonEntityDao;
+ this.locker = locker;
+ this.executor = executor;
+ this.tagInternalApi = tagInternalApi;
+ this.invoiceApi = invoiceApi;
+ }
+
+ protected boolean isAccountAutoPayOff(final UUID accountId, final InternalTenantContext context) {
+ final List<Tag> accountTags = tagInternalApi.getTags(accountId, ObjectType.ACCOUNT, context);
+
+ return ControlTagType.isAutoPayOff(Collections2.transform(accountTags, new Function<Tag, UUID>() {
+ @Nullable
+ @Override
+ public UUID apply(@Nullable final Tag tag) {
+ return tag.getTagDefinitionId();
+ }
+ }));
+ }
+
+ protected void setAccountAutoPayOff(final UUID accountId, final InternalCallContext context) throws PaymentApiException {
+ try {
+ tagInternalApi.addTag(accountId, ObjectType.ACCOUNT, ControlTagType.AUTO_PAY_OFF.getId(), context);
+ } catch (TagApiException e) {
+ log.error("Failed to add AUTO_PAY_OFF on account " + accountId, e);
+ throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, "Failed to add AUTO_PAY_OFF on account " + accountId);
+ }
+ }
+
+ public Set<String> getAvailablePlugins() {
+ return pluginRegistry.getAllServices();
+ }
+
+ protected PaymentPluginApi getPaymentPluginApi(final String pluginName) throws PaymentApiException {
+ final PaymentPluginApi pluginApi = pluginRegistry.getServiceForName(pluginName);
+ if (pluginApi == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_PLUGIN, pluginName);
+ }
+ return pluginApi;
+ }
+
+ protected PaymentPluginApi getPaymentProviderPlugin(final UUID paymentMethodId, final InternalTenantContext context) throws PaymentApiException {
+ final PaymentMethodModelDao methodDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodId, context);
+ if (methodDao == null) {
+ log.error("PaymentMethod does not exist", paymentMethodId);
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT_METHOD, paymentMethodId);
+ }
+ return getPaymentPluginApi(methodDao.getPluginName());
+ }
+
+ protected PaymentPluginApi getPaymentProviderPlugin(final Account account, final InternalTenantContext context) throws PaymentApiException {
+ final UUID paymentMethodId = account.getPaymentMethodId();
+ if (paymentMethodId == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD, account.getId());
+ }
+ return getPaymentProviderPlugin(paymentMethodId, context);
+ }
+
+ protected void postPaymentEvent(final BusInternalEvent ev, final UUID accountId, final InternalCallContext context) {
+ if (ev == null) {
+ return;
+ }
+ try {
+ eventBus.post(ev);
+ } catch (EventBusException e) {
+ log.error("Failed to post Payment event event for account {} ", accountId, e);
+ }
+ }
+
+ protected Invoice rebalanceAndGetInvoice(final UUID accountId, final UUID invoiceId, final InternalCallContext context) throws InvoiceApiException {
+ invoiceApi.consumeExistingCBAOnAccountWithUnpaidInvoices(accountId, context);
+ final Invoice invoice = invoiceApi.getInvoiceById(invoiceId, context);
+ return invoice;
+ }
+
+ protected TenantContext buildTenantContext(final InternalTenantContext context) {
+ return context.toTenantContext(nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT));
+ }
+
+ public interface WithAccountLockCallback<T> {
+
+ public T doOperation() throws PaymentApiException;
+ }
+
+ public static class CallableWithAccountLock<T> implements Callable<T> {
+
+ private final GlobalLocker locker;
+ private final String accountExternalKey;
+ private final WithAccountLockCallback<T> callback;
+
+ public CallableWithAccountLock(final GlobalLocker locker,
+ final String accountExternalKey,
+ final WithAccountLockCallback<T> callback) {
+ this.locker = locker;
+ this.accountExternalKey = accountExternalKey;
+ this.callback = callback;
+ }
+
+ @Override
+ public T call() throws Exception {
+ return new WithAccountLock<T>().processAccountWithLock(locker, accountExternalKey, callback);
+ }
+ }
+
+ public static class WithAccountLock<T> {
+
+ public T processAccountWithLock(final GlobalLocker locker, final String accountExternalKey, final WithAccountLockCallback<T> callback)
+ throws PaymentApiException {
+ GlobalLock lock = null;
+ try {
+ lock = locker.lockWithNumberOfTries(LockerType.ACCOUNT_FOR_INVOICE_PAYMENTS.toString(), accountExternalKey, NB_LOCK_TRY);
+ return callback.doOperation();
+ } catch (LockFailedException e) {
+ final String format = String.format("Failed to lock account %s", accountExternalKey);
+ log.error(String.format(format), e);
+ throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, format);
+ } finally {
+ if (lock != null) {
+ lock.release();
+ }
+ }
+ }
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/RefundProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/RefundProcessor.java
new file mode 100644
index 0000000..f621999
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/core/RefundProcessor.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.core;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.bus.api.PersistentBus;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.DefaultRefund;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.Refund;
+import org.killbill.billing.payment.api.RefundStatus;
+import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.billing.payment.dao.PaymentModelDao;
+import org.killbill.billing.payment.dao.RefundModelDao;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.billing.payment.plugin.api.RefundInfoPlugin;
+import org.killbill.billing.payment.plugin.api.RefundPluginStatus;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.EntityPaginationBuilder;
+import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.inject.name.Named;
+
+import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED;
+import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPagination;
+import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationFromPlugins;
+
+public class RefundProcessor extends ProcessorBase {
+
+ private static final Logger log = LoggerFactory.getLogger(RefundProcessor.class);
+
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public RefundProcessor(final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry,
+ final AccountInternalApi accountApi,
+ final InvoiceInternalApi invoiceApi,
+ final PersistentBus eventBus,
+ final InternalCallContextFactory internalCallContextFactory,
+ final TagInternalApi tagUserApi,
+ final PaymentDao paymentDao,
+ final NonEntityDao nonEntityDao,
+ final GlobalLocker locker,
+ @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor) {
+ super(pluginRegistry, accountApi, eventBus, paymentDao, nonEntityDao, tagUserApi, locker, executor, invoiceApi);
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ /**
+ * Create a refund and adjust the invoice or invoice items as necessary.
+ *
+ * @param account account to refund
+ * @param paymentId payment associated with that refund
+ * @param specifiedRefundAmount amount to refund. If null, the amount will be the sum of adjusted invoice items
+ * @param isAdjusted whether the refund should trigger an invoice or invoice item adjustment
+ * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
+ * @param context the call callcontext
+ * @return the created callcontext
+ * @throws PaymentApiException
+ */
+ public Refund createRefund(final Account account, final UUID paymentId, @Nullable final BigDecimal specifiedRefundAmount,
+ final boolean isAdjusted, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final InternalCallContext context)
+ throws PaymentApiException {
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT);
+
+ return new WithAccountLock<Refund>().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback<Refund>() {
+
+ @Override
+ public Refund doOperation() throws PaymentApiException {
+ // First, compute the refund amount, if necessary
+ final BigDecimal refundAmount = computeRefundAmount(paymentId, specifiedRefundAmount, invoiceItemIdsWithAmounts, context);
+
+ try {
+ final PaymentModelDao payment = paymentDao.getPayment(paymentId, context);
+ if (payment == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_SUCCESS_PAYMENT, paymentId);
+ }
+
+ final RefundModelDao refundInfo = new RefundModelDao(account.getId(), paymentId, refundAmount, account.getCurrency(), refundAmount, account.getCurrency(), isAdjusted);
+ paymentDao.insertRefund(refundInfo, context);
+
+ final PaymentPluginApi plugin = getPaymentProviderPlugin(payment.getPaymentMethodId(), context);
+ final RefundInfoPlugin refundInfoPlugin = plugin.processRefund(account.getId(), paymentId, refundAmount, account.getCurrency(), context.toCallContext(tenantId));
+
+ switch (refundInfoPlugin.getStatus()) {
+ case PROCESSED:
+ paymentDao.updateRefundStatus(refundInfo.getId(), RefundStatus.PLUGIN_COMPLETED, refundInfoPlugin.getAmount(), refundInfoPlugin.getCurrency(), context);
+
+ invoiceApi.createRefund(paymentId, refundAmount, isAdjusted, invoiceItemIdsWithAmounts, refundInfo.getId(), context);
+
+ paymentDao.updateRefundStatus(refundInfo.getId(), RefundStatus.COMPLETED, refundInfoPlugin.getAmount(), refundInfoPlugin.getCurrency(), context);
+
+ return new DefaultRefund(refundInfo.getId(), refundInfo.getCreatedDate(), refundInfo.getUpdatedDate(),
+ paymentId, refundInfo.getAmount(), account.getCurrency(),
+ isAdjusted, refundInfo.getCreatedDate(), RefundStatus.COMPLETED);
+
+ case PENDING:
+ paymentDao.updateRefundStatus(refundInfo.getId(), RefundStatus.PENDING, refundInfoPlugin.getAmount(), refundInfoPlugin.getCurrency(), context);
+ return new DefaultRefund(refundInfo.getId(), refundInfo.getCreatedDate(), refundInfo.getUpdatedDate(),
+ paymentId, refundInfo.getAmount(), account.getCurrency(),
+ isAdjusted, refundInfo.getCreatedDate(), RefundStatus.PENDING);
+
+ default:
+ paymentDao.updateRefundStatus(refundInfo.getId(), RefundStatus.PLUGIN_ERRORED, refundAmount, account.getCurrency(), context);
+ throw new PaymentPluginApiException("Refund error for RefundInfo: " + refundInfo.toString(),
+ String.format("Gateway error: %s, Gateway error code: %s, Reference ids: %s / %s",
+ refundInfoPlugin.getGatewayError(),
+ refundInfoPlugin.getGatewayErrorCode(),
+ refundInfoPlugin.getFirstRefundReferenceId(),
+ refundInfoPlugin.getSecondRefundReferenceId()));
+ }
+ } catch (PaymentPluginApiException e) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_CREATE_REFUND, account.getId(), e.getErrorMessage());
+ } catch (InvoiceApiException e) {
+ throw new PaymentApiException(e);
+ }
+ }
+ });
+ }
+
+ public void notifyPendingRefundOfStateChanged(final Account account, final UUID refundId, final boolean isSuccess, final InternalCallContext context)
+ throws PaymentApiException {
+
+ new WithAccountLock<Void>().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback<Void>() {
+
+ @Override
+ public Void doOperation() throws PaymentApiException {
+ try {
+ final RefundModelDao refund = paymentDao.getRefund(refundId, context);
+ if (refund == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_REFUND, refundId);
+ }
+ if (refund.getRefundStatus() != RefundStatus.PENDING) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NOT_PENDING, refundId);
+ }
+
+ // TODO STEPH : Model is broken if we had an invoice item adjustements as we lost track of them
+ invoiceApi.createRefund(refund.getPaymentId(), refund.getAmount(), refund.isAdjusted(), Collections.<UUID, BigDecimal>emptyMap(), refund.getId(), context);
+ paymentDao.updateRefundStatus(refund.getId(), RefundStatus.COMPLETED, refund.getAmount(), refund.getCurrency(), context);
+ } catch (InvoiceApiException e) {
+ }
+ return null;
+ }
+ });
+
+ }
+
+ /**
+ * Compute the refund amount (computed from the invoice or invoice items as necessary).
+ *
+ * @param paymentId payment id associated with this refund
+ * @param specifiedRefundAmount amount to refund. If null, the amount will be the sum of adjusted invoice items
+ * @param invoiceItemIdsWithAmounts invoice item ids and associated amounts to adjust
+ * @return the refund amount
+ */
+ private BigDecimal computeRefundAmount(final UUID paymentId, @Nullable final BigDecimal specifiedRefundAmount,
+ final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final InternalTenantContext context)
+ throws PaymentApiException {
+ try {
+ final List<InvoiceItem> items = invoiceApi.getInvoiceForPaymentId(paymentId, context).getInvoiceItems();
+
+ BigDecimal amountFromItems = BigDecimal.ZERO;
+ for (final UUID itemId : invoiceItemIdsWithAmounts.keySet()) {
+ amountFromItems = amountFromItems.add(Objects.firstNonNull(invoiceItemIdsWithAmounts.get(itemId),
+ getAmountFromItem(items, itemId)));
+ }
+
+ // Sanity check: if some items were specified, then the sum should be equal to specified refund amount, if specified
+ if (amountFromItems.compareTo(BigDecimal.ZERO) != 0 && specifiedRefundAmount != null && specifiedRefundAmount.compareTo(amountFromItems) != 0) {
+ throw new IllegalArgumentException("You can't specify a refund amount that doesn't match the invoice items amounts");
+ }
+
+ return Objects.firstNonNull(specifiedRefundAmount, amountFromItems);
+ } catch (InvoiceApiException e) {
+ throw new PaymentApiException(e);
+ }
+ }
+
+ private BigDecimal getAmountFromItem(final List<InvoiceItem> items, final UUID itemId) {
+ for (final InvoiceItem item : items) {
+ if (item.getId().equals(itemId)) {
+ return item.getAmount();
+ }
+ }
+
+ throw new IllegalArgumentException("Unable to find invoice item for id " + itemId);
+ }
+
+ public Refund getRefund(final UUID refundId, final boolean withPluginInfo, final InternalTenantContext context) throws PaymentApiException {
+ RefundModelDao result = paymentDao.getRefund(refundId, context);
+ if (result == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_REFUND, refundId);
+ }
+
+ final List<RefundModelDao> filteredInput = filterUncompletedPluginRefund(Collections.singletonList(result));
+ if (filteredInput.isEmpty()) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_REFUND, refundId);
+ }
+
+ if (completePluginCompletedRefund(filteredInput, context)) {
+ result = paymentDao.getRefund(refundId, context);
+ }
+
+ final PaymentModelDao payment = paymentDao.getPayment(result.getPaymentId(), context);
+ if (payment == null) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT, result.getPaymentId());
+ }
+
+ final PaymentPluginApi plugin = withPluginInfo ? getPaymentProviderPlugin(payment.getPaymentMethodId(), context) : null;
+ List<RefundInfoPlugin> refundInfoPlugins = ImmutableList.<RefundInfoPlugin>of();
+ if (plugin != null) {
+ try {
+ refundInfoPlugins = plugin.getRefundInfo(result.getAccountId(), result.getPaymentId(), buildTenantContext(context));
+ } catch (final PaymentPluginApiException e) {
+ throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_GET_REFUND_INFO, refundId, e.toString());
+ }
+ }
+
+ return new DefaultRefund(result, findRefundInfoPlugin(result, refundInfoPlugins));
+ }
+
+ private RefundInfoPlugin findRefundInfoPlugin(final RefundModelDao refundModelDao, final List<RefundInfoPlugin> refundInfoPlugins) {
+ // We have a mapping 1:N for payment:refunds and a mapping 1:1 for RefundModelDao:RefundInfoPlugin.
+ // Unfortunately, we processing a refund, we don't tell the plugin about the refund id, so we need to do some heuristics
+ // to map a RefundInfoPlugin back to its RefundModelDao
+ // TODO This will break for multiple partial refunds of the same amount. Check the effective date won't help for same day partial refunds and checking effective datetime seems risky
+ return Iterables.<RefundInfoPlugin>tryFind(refundInfoPlugins,
+ new Predicate<RefundInfoPlugin>() {
+ @Override
+ public boolean apply(final RefundInfoPlugin refundInfoPlugin) {
+ return refundObjectsMatch(refundModelDao, refundInfoPlugin);
+ }
+ }).orNull();
+ }
+
+ private boolean refundObjectsMatch(final RefundModelDao refundModelDao, final RefundInfoPlugin refundInfoPlugin) {
+ return (refundInfoPlugin.getKbPaymentId() != null && refundModelDao.getPaymentId() != null && refundInfoPlugin.getKbPaymentId().equals(refundModelDao.getPaymentId())) &&
+ (refundInfoPlugin.getAmount() != null && refundModelDao.getProcessedAmount() != null && refundInfoPlugin.getAmount().compareTo(refundModelDao.getProcessedAmount()) == 0) &&
+ (refundInfoPlugin.getCurrency() != null && refundModelDao.getProcessedCurrency() != null && refundInfoPlugin.getCurrency().equals(refundModelDao.getProcessedCurrency())) &&
+ (
+ (refundInfoPlugin.getStatus().equals(RefundPluginStatus.PROCESSED) && refundModelDao.getRefundStatus().equals(RefundStatus.COMPLETED)) ||
+ (refundInfoPlugin.getStatus().equals(RefundPluginStatus.PENDING) && refundModelDao.getRefundStatus().equals(RefundStatus.PENDING))
+ );
+ }
+
+ public Pagination<Refund> getRefunds(final Long offset, final Long limit, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) {
+ return getEntityPaginationFromPlugins(getAvailablePlugins(),
+ offset,
+ limit,
+ new EntityPaginationBuilder<Refund, PaymentApiException>() {
+ @Override
+ public Pagination<Refund> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+ return getRefunds(offset, limit, pluginName, tenantContext, internalTenantContext);
+ }
+ });
+ }
+
+ public Pagination<Refund> getRefunds(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+ final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+
+ return getEntityPagination(limit,
+ new SourcePaginationBuilder<RefundModelDao, PaymentApiException>() {
+ @Override
+ public Pagination<RefundModelDao> build() {
+ // Find all refunds for all accounts
+ return paymentDao.getRefunds(pluginName, offset, limit, internalTenantContext);
+ }
+ },
+ new Function<RefundModelDao, Refund>() {
+ @Override
+ public Refund apply(final RefundModelDao refundModelDao) {
+ List<RefundInfoPlugin> refundInfoPlugins = null;
+ try {
+ refundInfoPlugins = pluginApi.getRefundInfo(refundModelDao.getAccountId(), refundModelDao.getId(), tenantContext);
+ } catch (final PaymentPluginApiException e) {
+ log.warn("Unable to find refund id " + refundModelDao.getId() + " in plugin " + pluginName);
+ // We still want to return a refund object, even though the plugin details are missing
+ }
+
+ final RefundInfoPlugin refundInfoPlugin = refundInfoPlugins == null ? null : findRefundInfoPlugin(refundModelDao, refundInfoPlugins);
+ return new DefaultRefund(refundModelDao, refundInfoPlugin);
+ }
+ }
+ );
+ }
+
+ public Pagination<Refund> searchRefunds(final String searchKey, final Long offset, final Long limit, final InternalTenantContext internalTenantContext) {
+ return getEntityPaginationFromPlugins(getAvailablePlugins(),
+ offset,
+ limit,
+ new EntityPaginationBuilder<Refund, PaymentApiException>() {
+ @Override
+ public Pagination<Refund> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+ return searchRefunds(searchKey, offset, limit, pluginName, internalTenantContext);
+ }
+ });
+ }
+
+ public Pagination<Refund> searchRefunds(final String searchKey, final Long offset, final Long limit, final String pluginName, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+ final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+
+ final Map<UUID, List<RefundInfoPlugin>> refundsByPaymentId = new HashMap<UUID, List<RefundInfoPlugin>>();
+ final Map<UUID, List<RefundModelDao>> refundModelDaosByPaymentId = new HashMap<UUID, List<RefundModelDao>>();
+
+ return getEntityPagination(limit,
+ new SourcePaginationBuilder<RefundInfoPlugin, PaymentApiException>() {
+ @Override
+ public Pagination<RefundInfoPlugin> build() throws PaymentApiException {
+ final Pagination<RefundInfoPlugin> refunds;
+ try {
+ refunds = pluginApi.searchRefunds(searchKey, offset, limit, buildTenantContext(internalTenantContext));
+ } catch (final PaymentPluginApiException e) {
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_REFUNDS, pluginName, searchKey);
+ }
+
+ // We need to group the refunds from the plugin by payment id. Since the ordering of the results is unspecified,
+ // we cannot do streaming here unfortunately
+ for (final RefundInfoPlugin refundInfoPlugin : refunds) {
+ if (refundInfoPlugin.getKbPaymentId() == null) {
+ // Garbage from the plugin?
+ log.debug("Plugin {} returned a refund without a kbPaymentId for searchKey {}", pluginName, searchKey);
+ continue;
+ }
+
+ if (refundsByPaymentId.get(refundInfoPlugin.getKbPaymentId()) == null) {
+ refundsByPaymentId.put(refundInfoPlugin.getKbPaymentId(), new LinkedList<RefundInfoPlugin>());
+ }
+ refundsByPaymentId.get(refundInfoPlugin.getKbPaymentId()).add(refundInfoPlugin);
+ }
+
+ return refunds;
+ }
+ },
+ new Function<RefundInfoPlugin, Refund>() {
+ @Override
+ public Refund apply(final RefundInfoPlugin refundInfoPlugin) {
+ if (refundInfoPlugin.getKbPaymentId() == null) {
+ // Garbage from the plugin?
+ log.debug("Plugin {} returned a refund without a kbPaymentId for searchKey {}", pluginName, searchKey);
+ return null;
+ }
+
+ List<RefundModelDao> modelCandidates = refundModelDaosByPaymentId.get(refundInfoPlugin.getKbPaymentId());
+ if (modelCandidates == null) {
+ refundModelDaosByPaymentId.put(refundInfoPlugin.getKbPaymentId(), paymentDao.getRefundsForPayment(refundInfoPlugin.getKbPaymentId(), internalTenantContext));
+ modelCandidates = refundModelDaosByPaymentId.get(refundInfoPlugin.getKbPaymentId());
+ }
+
+ final RefundModelDao model = Iterables.<RefundModelDao>tryFind(modelCandidates,
+ new Predicate<RefundModelDao>() {
+ @Override
+ public boolean apply(final RefundModelDao refundModelDao) {
+ return refundObjectsMatch(refundModelDao, refundInfoPlugin);
+ }
+ }).orNull();
+
+ if (model == null) {
+ log.warn("Unable to find refund for payment id " + refundInfoPlugin.getKbPaymentId() + " present in plugin " + pluginName);
+ return null;
+ }
+
+ return new DefaultRefund(model, refundInfoPlugin);
+ }
+ }
+ );
+ }
+
+ public List<Refund> getAccountRefunds(final Account account, final InternalTenantContext context)
+ throws PaymentApiException {
+ List<RefundModelDao> result = paymentDao.getRefundsForAccount(account.getId(), context);
+ if (completePluginCompletedRefund(result, context)) {
+ result = paymentDao.getRefundsForAccount(account.getId(), context);
+ }
+ final List<RefundModelDao> filteredInput = filterUncompletedPluginRefund(result);
+ return toRefunds(filteredInput);
+ }
+
+ public List<Refund> getPaymentRefunds(final UUID paymentId, final InternalTenantContext context)
+ throws PaymentApiException {
+ List<RefundModelDao> result = paymentDao.getRefundsForPayment(paymentId, context);
+ if (completePluginCompletedRefund(result, context)) {
+ result = paymentDao.getRefundsForPayment(paymentId, context);
+ }
+ final List<RefundModelDao> filteredInput = filterUncompletedPluginRefund(result);
+ return toRefunds(filteredInput);
+ }
+
+ public List<Refund> toRefunds(final List<RefundModelDao> in) {
+ return new ArrayList<Refund>(Collections2.transform(in, new Function<RefundModelDao, Refund>() {
+ @Override
+ public Refund apply(final RefundModelDao cur) {
+ return new DefaultRefund(cur.getId(), cur.getCreatedDate(), cur.getUpdatedDate(),
+ cur.getPaymentId(), cur.getAmount(), cur.getCurrency(),
+ cur.isAdjusted(), cur.getCreatedDate(), cur.getRefundStatus());
+ }
+ }));
+ }
+
+ private List<RefundModelDao> filterUncompletedPluginRefund(final List<RefundModelDao> input) {
+ return new ArrayList<RefundModelDao>(Collections2.filter(input, new Predicate<RefundModelDao>() {
+ @Override
+ public boolean apply(final RefundModelDao in) {
+ return in.getRefundStatus() != RefundStatus.CREATED;
+ }
+ }));
+ }
+
+ private boolean completePluginCompletedRefund(final List<RefundModelDao> refunds, final InternalTenantContext tenantContext) throws PaymentApiException {
+
+ final Collection<RefundModelDao> refundsToBeFixed = Collections2.filter(refunds, new Predicate<RefundModelDao>() {
+ @Override
+ public boolean apply(final RefundModelDao in) {
+ return in.getRefundStatus() == RefundStatus.PLUGIN_COMPLETED;
+ }
+ });
+ if (refundsToBeFixed.size() == 0) {
+ return false;
+ }
+
+ try {
+
+ // TODO callcontext should be created for each refund and have the correct userToken
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(refundsToBeFixed.iterator().next().getId(), ObjectType.REFUND, "RefundProcessor",
+ CallOrigin.INTERNAL, UserType.SYSTEM, null);
+
+ final Account account = accountInternalApi.getAccountById(refundsToBeFixed.iterator().next().getAccountId(), context);
+ new WithAccountLock<Void>().processAccountWithLock(locker, account.getExternalKey(), new WithAccountLockCallback<Void>() {
+
+ @Override
+ public Void doOperation() throws PaymentApiException {
+ try {
+ for (final RefundModelDao cur : refundsToBeFixed) {
+
+ // TODO - we currently don't save the items to be adjusted. If we crash, they won't be adjusted...
+ invoiceApi.createRefund(cur.getPaymentId(), cur.getAmount(), cur.isAdjusted(), ImmutableMap.<UUID, BigDecimal>of(), cur.getId(), context);
+ paymentDao.updateRefundStatus(cur.getId(), RefundStatus.COMPLETED, cur.getProcessedAmount(), cur.getProcessedCurrency(), context);
+ }
+ } catch (InvoiceApiException e) {
+ throw new PaymentApiException(e);
+ }
+ return null;
+ }
+ });
+ return true;
+ } catch (AccountApiException e) {
+ throw new PaymentApiException(e);
+ }
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
new file mode 100644
index 0000000..f21c20d
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entity.EntityPersistenceException;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentMethod;
+import org.killbill.billing.payment.api.PaymentStatus;
+import org.killbill.billing.payment.api.Refund;
+import org.killbill.billing.payment.api.RefundStatus;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.DefaultPaginationSqlDaoHelper;
+import org.killbill.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+
+public class DefaultPaymentDao implements PaymentDao {
+
+ private final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
+ private final DefaultPaginationSqlDaoHelper paginationHelper;
+
+ @Inject
+ public DefaultPaymentDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ this.transactionalSqlDao = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao);
+ this.paginationHelper = new DefaultPaginationSqlDaoHelper(transactionalSqlDao);
+ }
+
+ @Override
+ public PaymentAttemptModelDao getPaymentAttempt(final UUID attemptId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentAttemptModelDao>() {
+ @Override
+ public PaymentAttemptModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(PaymentAttemptSqlDao.class).getById(attemptId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public PaymentModelDao insertPaymentWithFirstAttempt(final PaymentModelDao payment, final PaymentAttemptModelDao attempt, final InternalCallContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentModelDao>() {
+
+ @Override
+ public PaymentModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final PaymentSqlDao transactional = entitySqlDaoWrapperFactory.become(PaymentSqlDao.class);
+ transactional.create(payment, context);
+
+ entitySqlDaoWrapperFactory.become(PaymentAttemptSqlDao.class).create(attempt, context);
+
+ return transactional.getById(payment.getId().toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public PaymentAttemptModelDao updatePaymentWithNewAttempt(final UUID paymentId, final PaymentAttemptModelDao attempt, final InternalCallContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentAttemptModelDao>() {
+ @Override
+ public PaymentAttemptModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final PaymentAttemptSqlDao transactional = entitySqlDaoWrapperFactory.become(PaymentAttemptSqlDao.class);
+ transactional.create(attempt, context);
+ final PaymentAttemptModelDao savedAttempt = transactional.getById(attempt.getId().toString(), context);
+
+ entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).updatePaymentForNewAttempt(paymentId.toString(), attempt.getPaymentMethodId().toString(),
+ savedAttempt.getRequestedAmount(), attempt.getEffectiveDate().toDate(), context);
+
+ return savedAttempt;
+ }
+ });
+ }
+
+ @Override
+ public void updatePaymentAndAttemptOnCompletion(final UUID paymentId,
+ final PaymentStatus paymentStatus,
+ final BigDecimal processedAmount,
+ final Currency processedCurrency,
+ final UUID attemptId,
+ final String gatewayErrorCode,
+ final String gatewayErrorMsg,
+ final InternalCallContext context) {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).updatePaymentStatus(paymentId.toString(), processedAmount, processedCurrency, paymentStatus.toString(), context);
+ entitySqlDaoWrapperFactory.become(PaymentAttemptSqlDao.class).updatePaymentAttemptStatus(attemptId.toString(), paymentStatus.toString(), gatewayErrorCode, gatewayErrorMsg, context);
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public PaymentMethodModelDao insertPaymentMethod(final PaymentMethodModelDao paymentMethod, final InternalCallContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentMethodModelDao>() {
+ @Override
+ public PaymentMethodModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return insertPaymentMethodInTransaction(entitySqlDaoWrapperFactory, paymentMethod, context);
+ }
+ });
+ }
+
+ private PaymentMethodModelDao insertPaymentMethodInTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final PaymentMethodModelDao paymentMethod, final InternalCallContext context)
+ throws EntityPersistenceException {
+ final PaymentMethodSqlDao transactional = entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class);
+ transactional.create(paymentMethod, context);
+
+ return transactional.getById(paymentMethod.getId().toString(), context);
+ }
+
+ @Override
+ public RefundModelDao insertRefund(final RefundModelDao refundInfo, final InternalCallContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<RefundModelDao>() {
+
+ @Override
+ public RefundModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final RefundSqlDao transactional = entitySqlDaoWrapperFactory.become(RefundSqlDao.class);
+ transactional.create(refundInfo, context);
+ return transactional.getById(refundInfo.getId().toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public void updateRefundStatus(final UUID refundId, final RefundStatus refundStatus, final BigDecimal processedAmount, final Currency processedCurrency, final InternalCallContext context) {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ entitySqlDaoWrapperFactory.become(RefundSqlDao.class).updateStatus(refundId.toString(), refundStatus.toString(), processedAmount, processedCurrency, context);
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public Pagination<RefundModelDao> getRefunds(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+ return paginationHelper.getPagination(RefundSqlDao.class,
+ new PaginationIteratorBuilder<RefundModelDao, Refund, RefundSqlDao>() {
+ @Override
+ public Long getCount(final RefundSqlDao refundSqlDao, final InternalTenantContext context) {
+ return refundSqlDao.getCountByPluginName(pluginName, context);
+ }
+
+ @Override
+ public Iterator<RefundModelDao> build(final RefundSqlDao refundSqlDao, final Long limit, final InternalTenantContext context) {
+ return refundSqlDao.getByPluginName(pluginName, offset, limit, context);
+ }
+ },
+ offset,
+ limit,
+ context);
+ }
+
+ @Override
+ public RefundModelDao getRefund(final UUID refundId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<RefundModelDao>() {
+ @Override
+ public RefundModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(RefundSqlDao.class).getById(refundId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public List<RefundModelDao> getRefundsForPayment(final UUID paymentId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<RefundModelDao>>() {
+ @Override
+ public List<RefundModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(RefundSqlDao.class).getRefundsForPayment(paymentId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public List<RefundModelDao> getRefundsForAccount(final UUID accountId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<RefundModelDao>>() {
+ @Override
+ public List<RefundModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(RefundSqlDao.class).getRefundsForAccount(accountId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public PaymentMethodModelDao getPaymentMethod(final UUID paymentMethodId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentMethodModelDao>() {
+ @Override
+ public PaymentMethodModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class).getById(paymentMethodId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public PaymentMethodModelDao getPaymentMethodIncludedDeleted(final UUID paymentMethodId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentMethodModelDao>() {
+ @Override
+ public PaymentMethodModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class).getPaymentMethodIncludedDelete(paymentMethodId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public List<PaymentMethodModelDao> getPaymentMethods(final UUID accountId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<PaymentMethodModelDao>>() {
+ @Override
+ public List<PaymentMethodModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class).getByAccountId(accountId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public Pagination<PaymentMethodModelDao> getPaymentMethods(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+ return paginationHelper.getPagination(PaymentMethodSqlDao.class,
+ new PaginationIteratorBuilder<PaymentMethodModelDao, PaymentMethod, PaymentMethodSqlDao>() {
+ @Override
+ public Long getCount(final PaymentMethodSqlDao paymentMethodSqlDao, final InternalTenantContext context) {
+ return paymentMethodSqlDao.getCountByPluginName(pluginName, context);
+ }
+
+ @Override
+ public Iterator<PaymentMethodModelDao> build(final PaymentMethodSqlDao paymentMethodSqlDao, final Long limit, final InternalTenantContext context) {
+ return paymentMethodSqlDao.getByPluginName(pluginName, offset, limit, context);
+ }
+ },
+ offset,
+ limit,
+ context);
+ }
+
+ @Override
+ public void deletedPaymentMethod(final UUID paymentMethodId, final InternalCallContext context) {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ deletedPaymentMethodInTransaction(entitySqlDaoWrapperFactory, paymentMethodId, context);
+ return null;
+ }
+ });
+ }
+
+ private void deletedPaymentMethodInTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final UUID paymentMethodId, final InternalCallContext context) {
+ entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class).markPaymentMethodAsDeleted(paymentMethodId.toString(), context);
+ }
+
+ @Override
+ public void undeletedPaymentMethod(final UUID paymentMethodId, final InternalCallContext context) {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ undeletedPaymentMethodInTransaction(entitySqlDaoWrapperFactory, paymentMethodId, context);
+ return null;
+ }
+ });
+ }
+
+ private void undeletedPaymentMethodInTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final UUID paymentMethodId, final InternalCallContext context) {
+ final PaymentMethodSqlDao paymentMethodSqlDao = entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class);
+ paymentMethodSqlDao.unmarkPaymentMethodAsDeleted(paymentMethodId.toString(), context);
+ }
+
+ @Override
+ public List<PaymentModelDao> getPaymentsForInvoice(final UUID invoiceId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<PaymentModelDao>>() {
+ @Override
+ public List<PaymentModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).getPaymentsForInvoice(invoiceId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public PaymentModelDao getLastPaymentForPaymentMethod(final UUID accountId, final UUID paymentMethodId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentModelDao>() {
+ @Override
+ public PaymentModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).getLastPaymentForAccountAndPaymentMethod(accountId.toString(), paymentMethodId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public Pagination<PaymentModelDao> getPayments(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+ return paginationHelper.getPagination(PaymentSqlDao.class,
+ new PaginationIteratorBuilder<PaymentModelDao, Payment, PaymentSqlDao>() {
+ @Override
+ public Long getCount(final PaymentSqlDao paymentSqlDao, final InternalTenantContext context) {
+ return paymentSqlDao.getCountByPluginName(pluginName, context);
+ }
+
+ @Override
+ public Iterator<PaymentModelDao> build(final PaymentSqlDao paymentSqlDao, final Long limit, final InternalTenantContext context) {
+ return paymentSqlDao.getByPluginName(pluginName, offset, limit, context);
+ }
+ },
+ offset,
+ limit,
+ context);
+ }
+
+ @Override
+ public PaymentModelDao getPayment(final UUID paymentId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentModelDao>() {
+ @Override
+ public PaymentModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).getById(paymentId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public List<PaymentModelDao> getPaymentsForAccount(final UUID accountId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<PaymentModelDao>>() {
+ @Override
+ public List<PaymentModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).getPaymentsForAccount(accountId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public List<PaymentAttemptModelDao> getAttemptsForPayment(final UUID paymentId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<PaymentAttemptModelDao>>() {
+ @Override
+ public List<PaymentAttemptModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(PaymentAttemptSqlDao.class).getByPaymentId(paymentId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public List<PaymentMethodModelDao> refreshPaymentMethods(final UUID accountId, final String pluginName,
+ final List<PaymentMethodModelDao> newPaymentMethods, final InternalCallContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<PaymentMethodModelDao>>() {
+
+ @Override
+ public List<PaymentMethodModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final PaymentMethodSqlDao transactional = entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class);
+ // Look at all payment methods, including deleted ones. We assume that newPaymentMethods (payment methods returned by the plugin)
+ // is the full set of non-deleted payment methods in the plugin. If a payment method was marked as deleted on our side,
+ // but is still existing in the plugin, we will un-delete it.
+ final List<PaymentMethodModelDao> allPaymentMethodsForAccount = transactional.getByAccountIdIncludedDelete(accountId.toString(), context);
+
+ // Consider only the payment methods for the plugin we are refreshing
+ final Collection<PaymentMethodModelDao> existingPaymentMethods = Collections2.filter(allPaymentMethodsForAccount,
+ new Predicate<PaymentMethodModelDao>() {
+ @Override
+ public boolean apply(final PaymentMethodModelDao paymentMethod) {
+ return pluginName.equals(paymentMethod.getPluginName());
+ }
+ });
+
+ for (final PaymentMethodModelDao finalPaymentMethod : newPaymentMethods) {
+ PaymentMethodModelDao foundExistingPaymentMethod = null;
+ for (final PaymentMethodModelDao existingPaymentMethod : existingPaymentMethods) {
+ if (existingPaymentMethod.equals(finalPaymentMethod)) {
+ // We already have it - nothing to do
+ foundExistingPaymentMethod = existingPaymentMethod;
+ break;
+ } else if (existingPaymentMethod.equalsButActive(finalPaymentMethod)) {
+ // We already have it but its status has changed - update it accordingly
+ undeletedPaymentMethodInTransaction(entitySqlDaoWrapperFactory, existingPaymentMethod.getId(), context);
+ foundExistingPaymentMethod = existingPaymentMethod;
+ break;
+ }
+ // Otherwise, we don't have it
+ }
+
+ if (foundExistingPaymentMethod == null) {
+ insertPaymentMethodInTransaction(entitySqlDaoWrapperFactory, finalPaymentMethod, context);
+ } else {
+ existingPaymentMethods.remove(foundExistingPaymentMethod);
+ }
+ }
+
+ // Finally, all payment methods left in the existingPaymentMethods should be marked as deleted
+ for (final PaymentMethodModelDao existingPaymentMethod : existingPaymentMethods) {
+ // Need to verify if this is active -- failure to do so would provide an exception down the stream because
+ // the logic around audit/history will use getById to retrieve the entity and that method would not return
+ // a marked as deleted object
+ if (existingPaymentMethod.isActive()) {
+ deletedPaymentMethodInTransaction(entitySqlDaoWrapperFactory, existingPaymentMethod.getId(), context);
+ }
+ }
+ return transactional.getByAccountId(accountId.toString(), context);
+ }
+ });
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAttemptModelDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAttemptModelDao.java
new file mode 100644
index 0000000..56d4d01
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAttemptModelDao.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.api.PaymentAttempt;
+import org.killbill.billing.payment.api.PaymentStatus;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class PaymentAttemptModelDao extends EntityBase implements EntityModelDao<PaymentAttempt> {
+
+ private UUID accountId;
+ private UUID invoiceId;
+ private UUID paymentId;
+ private UUID paymentMethodId;
+ private PaymentStatus processingStatus;
+ private DateTime effectiveDate;
+ private String gatewayErrorCode;
+ private String gatewayErrorMsg;
+ private BigDecimal requestedAmount;
+ private Currency requestedCurrency;
+
+ public PaymentAttemptModelDao() { /* For the DAO mapper */ }
+
+ public PaymentAttemptModelDao(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate,
+ final UUID accountId, final UUID invoiceId,
+ final UUID paymentId, final UUID paymentMethodId,
+ final PaymentStatus processingStatus, final DateTime effectiveDate,
+ final BigDecimal requestedAmount, final Currency requestedCurrency,
+ final String gatewayErrorCode, final String gatewayErrorMsg) {
+ super(id, createdDate, updatedDate);
+ this.accountId = accountId;
+ this.invoiceId = invoiceId;
+ this.paymentId = paymentId;
+ this.paymentMethodId = paymentMethodId;
+ this.processingStatus = processingStatus;
+ this.effectiveDate = effectiveDate;
+ this.requestedAmount = requestedAmount;
+ this.requestedCurrency = requestedCurrency;
+ this.gatewayErrorCode = gatewayErrorCode;
+ this.gatewayErrorMsg = gatewayErrorMsg;
+ }
+
+ public PaymentAttemptModelDao(final UUID accountId, final UUID invoiceId, final UUID paymentId, final UUID paymentMethodId, final PaymentStatus paymentStatus, final DateTime effectiveDate,
+ final BigDecimal requestedAmount, final Currency requestedCurrency) {
+ this(UUID.randomUUID(), null, null, accountId, invoiceId, paymentId, paymentMethodId, paymentStatus, effectiveDate, requestedAmount, requestedCurrency, null, null);
+ }
+
+ public PaymentAttemptModelDao(final UUID accountId, final UUID invoiceId, final UUID paymentId, final UUID paymentMethodId, final DateTime effectiveDate,
+ final BigDecimal requestedAmount, final Currency requestedCurrency) {
+ this(UUID.randomUUID(), null, null, accountId, invoiceId, paymentId, paymentMethodId, PaymentStatus.UNKNOWN, effectiveDate, requestedAmount, requestedCurrency, null, null);
+ }
+
+ public PaymentAttemptModelDao(final PaymentAttemptModelDao src, final PaymentStatus newProcessingStatus, final String gatewayErrorCode, final String gatewayErrorMsg) {
+ this(src.getId(), src.getCreatedDate(), src.getUpdatedDate(), src.getAccountId(), src.getInvoiceId(), src.getPaymentId(), src.getPaymentMethodId(),
+ newProcessingStatus, src.getEffectiveDate(), src.getRequestedAmount(), src.getRequestedCurrency(), gatewayErrorCode, gatewayErrorMsg);
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ public UUID getPaymentId() {
+ return paymentId;
+ }
+
+ public UUID getPaymentMethodId() {
+ return paymentMethodId;
+ }
+
+ public PaymentStatus getProcessingStatus() {
+ return processingStatus;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public String getGatewayErrorCode() {
+ return gatewayErrorCode;
+ }
+
+ public String getGatewayErrorMsg() {
+ return gatewayErrorMsg;
+ }
+
+ public BigDecimal getRequestedAmount() {
+ return requestedAmount;
+ }
+
+ public Currency getRequestedCurrency() {
+ return requestedCurrency;
+ }
+
+ public void setAccountId(final UUID accountId) {
+ this.accountId = accountId;
+ }
+
+ public void setInvoiceId(final UUID invoiceId) {
+ this.invoiceId = invoiceId;
+ }
+
+ public void setPaymentId(final UUID paymentId) {
+ this.paymentId = paymentId;
+ }
+
+ public void setPaymentMethodId(final UUID paymentMethodId) {
+ this.paymentMethodId = paymentMethodId;
+ }
+
+ public void setProcessingStatus(final PaymentStatus processingStatus) {
+ this.processingStatus = processingStatus;
+ }
+
+ public void setEffectiveDate(final DateTime effectiveDate) {
+ this.effectiveDate = effectiveDate;
+ }
+
+ public void setGatewayErrorCode(final String gatewayErrorCode) {
+ this.gatewayErrorCode = gatewayErrorCode;
+ }
+
+ public void setGatewayErrorMsg(final String gatewayErrorMsg) {
+ this.gatewayErrorMsg = gatewayErrorMsg;
+ }
+
+ public void setRequestedAmount(final BigDecimal requestedAmount) {
+ this.requestedAmount = requestedAmount;
+ }
+
+ public void setRequestedCurrency(final Currency requestedCurrency) {
+ this.requestedCurrency = requestedCurrency;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("PaymentAttemptModelDao");
+ sb.append("{accountId=").append(accountId);
+ sb.append(", invoiceId=").append(invoiceId);
+ sb.append(", paymentId=").append(paymentId);
+ sb.append(", processingStatus=").append(processingStatus);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", gatewayErrorCode='").append(gatewayErrorCode).append('\'');
+ sb.append(", gatewayErrorMsg='").append(gatewayErrorMsg).append('\'');
+ sb.append(", requestedAmount=").append(requestedAmount);
+ sb.append(", requestedCurrency=").append(requestedCurrency);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final PaymentAttemptModelDao that = (PaymentAttemptModelDao) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+ return false;
+ }
+ if (gatewayErrorCode != null ? !gatewayErrorCode.equals(that.gatewayErrorCode) : that.gatewayErrorCode != null) {
+ return false;
+ }
+ if (gatewayErrorMsg != null ? !gatewayErrorMsg.equals(that.gatewayErrorMsg) : that.gatewayErrorMsg != null) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
+ return false;
+ }
+ if (paymentMethodId != null ? !paymentMethodId.equals(that.paymentMethodId) : that.paymentMethodId != null) {
+ return false;
+ }
+ if (processingStatus != that.processingStatus) {
+ return false;
+ }
+ if (requestedAmount != null ? !requestedAmount.equals(that.requestedAmount) : that.requestedAmount != null) {
+ return false;
+ }
+ if (requestedCurrency != that.requestedCurrency) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (invoiceId != null ? invoiceId.hashCode() : 0);
+ result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
+ result = 31 * result + (paymentMethodId != null ? paymentMethodId.hashCode() : 0);
+ result = 31 * result + (processingStatus != null ? processingStatus.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (gatewayErrorCode != null ? gatewayErrorCode.hashCode() : 0);
+ result = 31 * result + (gatewayErrorMsg != null ? gatewayErrorMsg.hashCode() : 0);
+ result = 31 * result + (requestedAmount != null ? requestedAmount.hashCode() : 0);
+ result = 31 * result + (requestedCurrency != null ? requestedCurrency.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.PAYMENT_ATTEMPTS;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return TableName.PAYMENT_ATTEMPT_HISTORY;
+ }
+
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAttemptSqlDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAttemptSqlDao.java
new file mode 100644
index 0000000..01355ca
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAttemptSqlDao.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.payment.api.PaymentAttempt;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface PaymentAttemptSqlDao extends EntitySqlDao<PaymentAttemptModelDao, PaymentAttempt> {
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ void updatePaymentAttemptStatus(@Bind("id") final String attemptId,
+ @Bind("processingStatus") final String processingStatus,
+ @Bind("gatewayErrorCode") final String gatewayErrorCode,
+ @Bind("gatewayErrorMsg") final String gatewayErrorMsg,
+ @BindBean final InternalCallContext context);
+
+ @SqlQuery
+ List<PaymentAttemptModelDao> getByPaymentId(@Bind("paymentId") final String paymentId,
+ @BindBean final InternalTenantContext context);
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
new file mode 100644
index 0000000..7649fc0
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.api.PaymentStatus;
+import org.killbill.billing.payment.api.RefundStatus;
+import org.killbill.billing.util.entity.Pagination;
+
+public interface PaymentDao {
+
+ public PaymentModelDao insertPaymentWithFirstAttempt(PaymentModelDao paymentInfo, PaymentAttemptModelDao attempt, InternalCallContext context);
+
+ public PaymentAttemptModelDao updatePaymentWithNewAttempt(UUID paymentId, PaymentAttemptModelDao attempt, InternalCallContext context);
+
+ public void updatePaymentAndAttemptOnCompletion(UUID paymentId, PaymentStatus paymentStatus,
+ BigDecimal processedAmount, Currency processedCurrency,
+ UUID attemptId, String gatewayErrorMsg, String gatewayErrorCode, InternalCallContext context);
+
+ public PaymentAttemptModelDao getPaymentAttempt(UUID attemptId, InternalTenantContext context);
+
+ public List<PaymentModelDao> getPaymentsForInvoice(UUID invoiceId, InternalTenantContext context);
+
+ public List<PaymentModelDao> getPaymentsForAccount(UUID accountId, InternalTenantContext context);
+
+ public PaymentModelDao getLastPaymentForPaymentMethod(UUID accountId, UUID paymentMethodId, InternalTenantContext context);
+
+ public Pagination<PaymentModelDao> getPayments(String pluginName, Long offset, Long limit, InternalTenantContext context);
+
+ public PaymentModelDao getPayment(UUID paymentId, InternalTenantContext context);
+
+ public List<PaymentAttemptModelDao> getAttemptsForPayment(UUID paymentId, InternalTenantContext context);
+
+ public RefundModelDao insertRefund(RefundModelDao refundInfo, InternalCallContext context);
+
+ public void updateRefundStatus(UUID refundId, RefundStatus status, BigDecimal processedAmount, Currency processedCurrency, InternalCallContext context);
+
+ public Pagination<RefundModelDao> getRefunds(String pluginName, Long offset, Long limit, InternalTenantContext context);
+
+ public RefundModelDao getRefund(UUID refundId, InternalTenantContext context);
+
+ public List<RefundModelDao> getRefundsForPayment(UUID paymentId, InternalTenantContext context);
+
+ public List<RefundModelDao> getRefundsForAccount(UUID accountId, InternalTenantContext context);
+
+ public PaymentMethodModelDao insertPaymentMethod(PaymentMethodModelDao paymentMethod, InternalCallContext context);
+
+ public PaymentMethodModelDao getPaymentMethod(UUID paymentMethodId, InternalTenantContext context);
+
+ public PaymentMethodModelDao getPaymentMethodIncludedDeleted(UUID paymentMethodId, InternalTenantContext context);
+
+ public List<PaymentMethodModelDao> getPaymentMethods(UUID accountId, InternalTenantContext context);
+
+ public Pagination<PaymentMethodModelDao> getPaymentMethods(String pluginName, Long offset, Long limit, InternalTenantContext context);
+
+ public void deletedPaymentMethod(UUID paymentMethodId, InternalCallContext context);
+
+ public List<PaymentMethodModelDao> refreshPaymentMethods(UUID accountId, String pluginName, List<PaymentMethodModelDao> paymentMethods, InternalCallContext context);
+
+ public void undeletedPaymentMethod(UUID paymentMethodId, InternalCallContext context);
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentMethodModelDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentMethodModelDao.java
new file mode 100644
index 0000000..7d0e05e
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentMethodModelDao.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.payment.api.PaymentMethod;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class PaymentMethodModelDao extends EntityBase implements EntityModelDao<PaymentMethod> {
+
+ private UUID accountId;
+ private String pluginName;
+ private Boolean isActive;
+
+ public PaymentMethodModelDao() { /* For the DAO mapper */ }
+
+ public PaymentMethodModelDao(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate,
+ final UUID accountId, final String pluginName,
+ final Boolean isActive) {
+ super(id, createdDate, updatedDate);
+ this.accountId = accountId;
+ this.pluginName = pluginName;
+ this.isActive = isActive;
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public String getPluginName() {
+ return pluginName;
+ }
+
+ // TODO Required for making the BindBeanFactory with Introspector work
+ public Boolean getIsActive() {
+ return isActive;
+ }
+
+ public Boolean isActive() {
+ return isActive;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("PaymentMethodModelDao");
+ sb.append("{accountId=").append(accountId);
+ sb.append(", pluginName='").append(pluginName).append('\'');
+ sb.append(", isActive=").append(isActive);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final PaymentMethodModelDao that = (PaymentMethodModelDao) o;
+
+ if (!equalsButActive(that)) {
+ return false;
+ }
+
+ if (isActive != null ? !isActive.equals(that.isActive) : that.isActive != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean equalsButActive(final PaymentMethodModelDao that) {
+ if (id != null ? !id.equals(that.id) : that.id != null) {
+ return false;
+ }
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (pluginName != null ? !pluginName.equals(that.pluginName) : that.pluginName != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = accountId != null ? accountId.hashCode() : 0;
+ result = 31 * result + (pluginName != null ? pluginName.hashCode() : 0);
+ result = 31 * result + (isActive != null ? isActive.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.PAYMENT_METHODS;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return TableName.PAYMENT_METHOD_HISTORY;
+ }
+
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentMethodSqlDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentMethodSqlDao.java
new file mode 100644
index 0000000..659d086
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentMethodSqlDao.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.commons.jdbi.statement.SmartFetchSize;
+import org.killbill.billing.payment.api.PaymentMethod;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface PaymentMethodSqlDao extends EntitySqlDao<PaymentMethodModelDao, PaymentMethod> {
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ void markPaymentMethodAsDeleted(@Bind("id") final String paymentMethodId,
+ @BindBean final InternalCallContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ void unmarkPaymentMethodAsDeleted(@Bind("id") final String paymentMethodId,
+ @BindBean final InternalCallContext context);
+
+ @SqlQuery
+ PaymentMethodModelDao getPaymentMethodIncludedDelete(@Bind("id") final String paymentMethodId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ List<PaymentMethodModelDao> getByAccountId(@Bind("accountId") final String accountId, @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ List<PaymentMethodModelDao> getByAccountIdIncludedDelete(@Bind("accountId") final String accountId, @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @SmartFetchSize(shouldStream = true)
+ public Iterator<PaymentMethodModelDao> getByPluginName(@Bind("pluginName") final String pluginName,
+ @Bind("offset") final Long offset,
+ @Bind("rowCount") final Long rowCount,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public Long getCountByPluginName(@Bind("pluginName") final String pluginName,
+ @BindBean final InternalTenantContext context);
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentModelDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentModelDao.java
new file mode 100644
index 0000000..1a970b1
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentModelDao.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentStatus;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class PaymentModelDao extends EntityBase implements EntityModelDao<Payment> {
+
+ public static final Integer INVALID_PAYMENT_NUMBER = new Integer(-13);
+
+ private UUID accountId;
+ private UUID invoiceId;
+ private UUID paymentMethodId;
+ private BigDecimal amount;
+ private Currency currency;
+ private BigDecimal processedAmount;
+ private Currency processedCurrency;
+ private DateTime effectiveDate;
+ private Integer paymentNumber;
+ private PaymentStatus paymentStatus;
+ private String extFirstPaymentRefId;
+ private String extSecondPaymentRefId;
+
+ public PaymentModelDao() { /* For the DAO mapper */ }
+
+ public PaymentModelDao(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate, final UUID accountId,
+ final UUID invoiceId, final UUID paymentMethodId,
+ final Integer paymentNumber, final BigDecimal amount, final Currency currency, final BigDecimal processedAmount, final Currency processedCurrency,
+ final PaymentStatus paymentStatus, final DateTime effectiveDate, final String extFirstPaymentRefId, final String extSecondPaymentRefId) {
+ super(id, createdDate, updatedDate);
+ this.accountId = accountId;
+ this.invoiceId = invoiceId;
+ this.paymentMethodId = paymentMethodId;
+ this.paymentNumber = paymentNumber;
+ this.amount = amount;
+ this.currency = currency;
+ this.processedAmount = processedAmount;
+ this.processedCurrency = processedCurrency;
+ this.paymentStatus = paymentStatus;
+ this.effectiveDate = effectiveDate;
+ this.extFirstPaymentRefId = extFirstPaymentRefId;
+ this.extSecondPaymentRefId = extSecondPaymentRefId;
+ }
+
+ public PaymentModelDao(final UUID accountId, final UUID invoiceId, final UUID paymentMethodId,
+ final BigDecimal amount, final Currency currency, final DateTime effectiveDate, final PaymentStatus paymentStatus) {
+ this(UUID.randomUUID(), null, null, accountId, invoiceId, paymentMethodId, INVALID_PAYMENT_NUMBER, amount, currency, amount, currency, paymentStatus, effectiveDate, null, null);
+ }
+
+ public PaymentModelDao(final UUID accountId, final UUID invoiceId, final UUID paymentMethodId,
+ final BigDecimal amount, final Currency currency, final DateTime effectiveDate) {
+ this(UUID.randomUUID(), null, null, accountId, invoiceId, paymentMethodId, INVALID_PAYMENT_NUMBER, amount, currency, amount, currency, PaymentStatus.UNKNOWN, effectiveDate, null, null);
+ }
+
+ public PaymentModelDao(final PaymentModelDao src, final PaymentStatus newPaymentStatus) {
+ this(src.getId(), src.getCreatedDate(), src.getUpdatedDate(), src.getAccountId(), src.getInvoiceId(), src.getPaymentMethodId(),
+ src.getPaymentNumber(), src.getAmount(), src.getCurrency(), src.getProcessedAmount(), src.getProcessedCurrency(), newPaymentStatus, src.getEffectiveDate(), null, null);
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ public UUID getPaymentMethodId() {
+ return paymentMethodId;
+ }
+
+ public Integer getPaymentNumber() {
+ return paymentNumber;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ public BigDecimal getProcessedAmount() {
+ return processedAmount;
+ }
+
+ public Currency getProcessedCurrency() {
+ return processedCurrency;
+ }
+
+ public PaymentStatus getPaymentStatus() {
+ return paymentStatus;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public String getExtFirstPaymentRefId() {
+ return extFirstPaymentRefId;
+ }
+
+ public String getExtSecondPaymentRefId() {
+ return extSecondPaymentRefId;
+ }
+
+ public void setAccountId(final UUID accountId) {
+ this.accountId = accountId;
+ }
+
+ public void setInvoiceId(final UUID invoiceId) {
+ this.invoiceId = invoiceId;
+ }
+
+ public void setPaymentMethodId(final UUID paymentMethodId) {
+ this.paymentMethodId = paymentMethodId;
+ }
+
+ public void setAmount(final BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public void setCurrency(final Currency currency) {
+ this.currency = currency;
+ }
+
+ public void setProcessedAmount(final BigDecimal processedAmount) {
+ this.processedAmount = processedAmount;
+ }
+
+ public void setProcessedCurrency(final Currency processedCurrency) {
+ this.processedCurrency = processedCurrency;
+ }
+
+ public void setEffectiveDate(final DateTime effectiveDate) {
+ this.effectiveDate = effectiveDate;
+ }
+
+ public void setPaymentNumber(final Integer paymentNumber) {
+ this.paymentNumber = paymentNumber;
+ }
+
+ public void setPaymentStatus(final PaymentStatus paymentStatus) {
+ this.paymentStatus = paymentStatus;
+ }
+
+ public void setExtFirstPaymentRefId(final String extFirstPaymentRefId) {
+ this.extFirstPaymentRefId = extFirstPaymentRefId;
+ }
+
+ public void setExtSecondPaymentRefId(final String extSecondPaymentRefId) {
+ this.extSecondPaymentRefId = extSecondPaymentRefId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("PaymentModelDao");
+ sb.append("{accountId=").append(accountId);
+ sb.append(", invoiceId=").append(invoiceId);
+ sb.append(", paymentMethodId=").append(paymentMethodId);
+ sb.append(", amount=").append(amount);
+ sb.append(", currency=").append(currency);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", paymentNumber=").append(paymentNumber);
+ sb.append(", paymentStatus=").append(paymentStatus);
+ sb.append(", extFirstPaymentRefId='").append(extFirstPaymentRefId).append('\'');
+ sb.append(", extSecondPaymentRefId='").append(extSecondPaymentRefId).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final PaymentModelDao that = (PaymentModelDao) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (amount != null ? !amount.equals(that.amount) : that.amount != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (processedAmount != null ? !processedAmount.equals(that.processedAmount) : that.processedAmount != null) {
+ return false;
+ }
+ if (processedCurrency != that.processedCurrency) {
+ return false;
+ }
+ if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+ return false;
+ }
+ if (extFirstPaymentRefId != null ? !extFirstPaymentRefId.equals(that.extFirstPaymentRefId) : that.extFirstPaymentRefId != null) {
+ return false;
+ }
+ if (extSecondPaymentRefId != null ? !extSecondPaymentRefId.equals(that.extSecondPaymentRefId) : that.extSecondPaymentRefId != null) {
+ return false;
+ }
+ if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
+ return false;
+ }
+ if (paymentMethodId != null ? !paymentMethodId.equals(that.paymentMethodId) : that.paymentMethodId != null) {
+ return false;
+ }
+ if (paymentNumber != null ? !paymentNumber.equals(that.paymentNumber) : that.paymentNumber != null) {
+ return false;
+ }
+ if (paymentStatus != that.paymentStatus) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (invoiceId != null ? invoiceId.hashCode() : 0);
+ result = 31 * result + (paymentMethodId != null ? paymentMethodId.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (processedAmount != null ? processedAmount.hashCode() : 0);
+ result = 31 * result + (processedCurrency != null ? processedCurrency.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (paymentNumber != null ? paymentNumber.hashCode() : 0);
+ result = 31 * result + (paymentStatus != null ? paymentStatus.hashCode() : 0);
+ result = 31 * result + (extFirstPaymentRefId != null ? extFirstPaymentRefId.hashCode() : 0);
+ result = 31 * result + (extSecondPaymentRefId != null ? extSecondPaymentRefId.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.PAYMENTS;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return TableName.PAYMENT_HISTORY;
+ }
+
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java
new file mode 100644
index 0000000..3783c86
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.commons.jdbi.statement.SmartFetchSize;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface PaymentSqlDao extends EntitySqlDao<PaymentModelDao, Payment> {
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ void updatePaymentStatus(@Bind("id") final String paymentId,
+ @Bind("processedAmount") final BigDecimal processedAmount,
+ @Bind("processedCurrency") final Currency processedCurrency,
+ @Bind("paymentStatus") final String paymentStatus,
+ @BindBean final InternalCallContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ void updatePaymentForNewAttempt(@Bind("id") final String paymentId,
+ @Bind("paymentMethodId") final String paymentMethodId,
+ @Bind("amount") final BigDecimal amount,
+ @Bind("effectiveDate") final Date effectiveDate,
+ @BindBean final InternalCallContext context);
+
+ @SqlQuery
+ PaymentModelDao getLastPaymentForAccountAndPaymentMethod(@Bind("accountId") final String accountId,
+ @Bind("paymentMethodId") final String paymentMethodId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ List<PaymentModelDao> getPaymentsForInvoice(@Bind("invoiceId") final String invoiceId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ List<PaymentModelDao> getPaymentsForAccount(@Bind("accountId") final String accountId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @SmartFetchSize(shouldStream = true)
+ public Iterator<PaymentModelDao> getByPluginName(@Bind("pluginName") final String pluginName,
+ @Bind("offset") final Long offset,
+ @Bind("rowCount") final Long rowCount,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public Long getCountByPluginName(@Bind("pluginName") final String pluginName,
+ @BindBean final InternalTenantContext context);
+}
+
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/RefundModelDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/RefundModelDao.java
new file mode 100644
index 0000000..c41202c
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/RefundModelDao.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.payment.api.Refund;
+import org.killbill.billing.payment.api.RefundStatus;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class RefundModelDao extends EntityBase implements EntityModelDao<Refund> {
+
+ private UUID accountId;
+ private UUID paymentId;
+ private BigDecimal amount;
+ private Currency currency;
+ private BigDecimal processedAmount;
+ private Currency processedCurrency;
+ private boolean isAdjusted;
+ private RefundStatus refundStatus;
+
+ public RefundModelDao() { /* For the DAO mapper */ }
+
+ public RefundModelDao(final UUID accountId, final UUID paymentId, final BigDecimal amount, final Currency currency,
+ final BigDecimal processedAmount, final Currency processedCurrency, final boolean isAdjusted) {
+ this(UUID.randomUUID(), accountId, paymentId, amount, currency, processedAmount, processedCurrency, isAdjusted, RefundStatus.CREATED, null, null);
+ }
+
+ public RefundModelDao(final UUID id, final UUID accountId, final UUID paymentId, final BigDecimal amount,
+ final Currency currency, final BigDecimal processedAmount, final Currency processedCurrency,
+ final boolean isAdjusted, final RefundStatus refundStatus,
+ @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate) {
+ super(id, createdDate, updatedDate);
+ this.accountId = accountId;
+ this.paymentId = paymentId;
+ this.amount = amount;
+ this.currency = currency;
+ this.processedAmount = processedAmount;
+ this.processedCurrency = processedCurrency;
+ this.refundStatus = refundStatus;
+ this.isAdjusted = isAdjusted;
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public UUID getPaymentId() {
+ return paymentId;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ public BigDecimal getProcessedAmount() {
+ return processedAmount;
+ }
+
+ public Currency getProcessedCurrency() {
+ return processedCurrency;
+ }
+
+ public RefundStatus getRefundStatus() {
+ return refundStatus;
+ }
+
+ // TODO Required for making the BindBeanFactory with Introspector work
+ // see Introspector line 571; they look at public method.
+ public boolean getIsAdjusted() {
+ return isAdjusted;
+ }
+
+ public boolean isAdjusted() {
+ return isAdjusted;
+ }
+
+ public void setAccountId(final UUID accountId) {
+ this.accountId = accountId;
+ }
+
+ public void setPaymentId(final UUID paymentId) {
+ this.paymentId = paymentId;
+ }
+
+ public void setAmount(final BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ public void setCurrency(final Currency currency) {
+ this.currency = currency;
+ }
+
+ public void setProcessedAmount(final BigDecimal processedAmount) {
+ this.processedAmount = processedAmount;
+ }
+
+ public void setProcessedCurrency(final Currency processedCurrency) {
+ this.processedCurrency = processedCurrency;
+ }
+
+ public void setIsAdjusted(final boolean isAdjusted) {
+ this.isAdjusted = isAdjusted;
+ }
+
+ public void setRefundStatus(final RefundStatus refundStatus) {
+ this.refundStatus = refundStatus;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("RefundModelDao");
+ sb.append("{accountId=").append(accountId);
+ sb.append(", paymentId=").append(paymentId);
+ sb.append(", amount=").append(amount);
+ sb.append(", currency=").append(currency);
+ sb.append(", processedAmount=").append(processedAmount);
+ sb.append(", processedCurrency=").append(processedCurrency);
+ sb.append(", isAdjusted=").append(isAdjusted);
+ sb.append(", refundStatus=").append(refundStatus);
+ sb.append(", createdDate=").append(createdDate);
+ sb.append(", updatedDate=").append(updatedDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final RefundModelDao that = (RefundModelDao) o;
+
+ if (isAdjusted != that.isAdjusted) {
+ return false;
+ }
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (amount != null ? !amount.equals(that.amount) : that.amount != null) {
+ return false;
+ }
+ if (processedAmount != null ? !processedAmount.equals(that.processedAmount) : that.processedAmount != null) {
+ return false;
+ }
+ if (createdDate != null ? !createdDate.equals(that.createdDate) : that.createdDate != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (processedCurrency != that.processedCurrency) {
+ return false;
+ }
+ if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
+ return false;
+ }
+ if (refundStatus != that.refundStatus) {
+ return false;
+ }
+ if (updatedDate != null ? !updatedDate.equals(that.updatedDate) : that.updatedDate != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = accountId != null ? accountId.hashCode() : 0;
+ result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (processedAmount != null ? processedAmount.hashCode() : 0);
+ result = 31 * result + (processedCurrency != null ? processedCurrency.hashCode() : 0);
+ result = 31 * result + (isAdjusted ? 1 : 0);
+ result = 31 * result + (refundStatus != null ? refundStatus.hashCode() : 0);
+ result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+ result = 31 * result + (updatedDate != null ? updatedDate.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.REFUNDS;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return TableName.REFUND_HISTORY;
+ }
+
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/RefundSqlDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/RefundSqlDao.java
new file mode 100644
index 0000000..582c26b
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/RefundSqlDao.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.math.BigDecimal;
+import java.util.Iterator;
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.commons.jdbi.statement.SmartFetchSize;
+import org.killbill.billing.payment.api.Refund;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface RefundSqlDao extends EntitySqlDao<RefundModelDao, Refund> {
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ void updateStatus(@Bind("id") final String refundId,
+ @Bind("refundStatus") final String status,
+ @Bind("processedAmount") final BigDecimal processedAmount,
+ @Bind("processedCurrency") final Currency processedCurrency,
+ @BindBean final InternalCallContext context);
+
+ @SqlQuery
+ List<RefundModelDao> getRefundsForPayment(@Bind("paymentId") final String paymentId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ List<RefundModelDao> getRefundsForAccount(@Bind("accountId") final String accountId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @SmartFetchSize(shouldStream = true)
+ public Iterator<RefundModelDao> getByPluginName(@Bind("pluginName") final String pluginName,
+ @Bind("offset") final Long offset,
+ @Bind("rowCount") final Long rowCount,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public Long getCountByPluginName(@Bind("pluginName") final String pluginName,
+ @BindBean final InternalTenantContext context);
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java
new file mode 100644
index 0000000..6bb30d0
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.payment.dispatcher;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.payment.api.PaymentApiException;
+
+public class PluginDispatcher<T> {
+
+ private static final Logger log = LoggerFactory.getLogger(PluginDispatcher.class);
+
+ private final TimeUnit DEEFAULT_PLUGIN_TIMEOUT_UNIT = TimeUnit.SECONDS;
+
+ private final long timeoutSeconds;
+ private final ExecutorService executor;
+
+ public PluginDispatcher(final long tiemoutSeconds, final ExecutorService executor) {
+ this.timeoutSeconds = tiemoutSeconds;
+ this.executor = executor;
+ }
+
+
+ public T dispatchWithAccountLock(final Callable<T> task)
+ throws PaymentApiException, TimeoutException {
+ return dispatchWithAccountLockAndTimeout(task, timeoutSeconds, DEEFAULT_PLUGIN_TIMEOUT_UNIT);
+ }
+
+ public T dispatchWithAccountLockAndTimeout(final Callable<T> task, final long timeout, final TimeUnit unit)
+ throws PaymentApiException, TimeoutException {
+
+ try {
+ final Future<T> future = executor.submit(task);
+ return future.get(timeout, unit);
+ } catch (ExecutionException e) {
+ if (e.getCause() instanceof PaymentApiException) {
+ throw (PaymentApiException) e.getCause();
+ } else {
+ throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, e.getMessage());
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, e.getMessage());
+ }
+ }
+
+
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentProviderPluginRegistryProvider.java b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentProviderPluginRegistryProvider.java
new file mode 100644
index 0000000..9d0f51b
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentProviderPluginRegistryProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.glue;
+
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.util.config.PaymentConfig;
+import org.killbill.billing.payment.provider.DefaultPaymentProviderPluginRegistry;
+import org.killbill.billing.payment.provider.ExternalPaymentProviderPlugin;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class DefaultPaymentProviderPluginRegistryProvider implements Provider<OSGIServiceRegistration<PaymentPluginApi>> {
+
+ private final PaymentConfig paymentConfig;
+ private final ExternalPaymentProviderPlugin externalPaymentProviderPlugin;
+
+ @Inject
+ public DefaultPaymentProviderPluginRegistryProvider(final PaymentConfig paymentConfig, final ExternalPaymentProviderPlugin externalPaymentProviderPlugin) {
+ this.paymentConfig = paymentConfig;
+ this.externalPaymentProviderPlugin = externalPaymentProviderPlugin;
+ }
+
+ @Override
+ public OSGIServiceRegistration<PaymentPluginApi> get() {
+ final DefaultPaymentProviderPluginRegistry pluginRegistry = new DefaultPaymentProviderPluginRegistry(paymentConfig);
+
+ // Make the external payment provider plugin available by default
+ final OSGIServiceDescriptor desc = new OSGIServiceDescriptor() {
+ @Override
+ public String getPluginSymbolicName() {
+ return null;
+ }
+ @Override
+ public String getRegistrationName() {
+ return ExternalPaymentProviderPlugin.PLUGIN_NAME;
+ }
+ };
+ pluginRegistry.registerService(desc, externalPaymentProviderPlugin);
+
+ return pluginRegistry;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
new file mode 100644
index 0000000..4ef9a43
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/DefaultPaymentService.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.glue;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.lifecycle.LifecycleHandlerType;
+import org.killbill.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentService;
+import org.killbill.billing.payment.bus.InvoiceHandler;
+import org.killbill.billing.payment.bus.PaymentTagHandler;
+import org.killbill.billing.payment.retry.AutoPayRetryService;
+import org.killbill.billing.payment.retry.FailedPaymentRetryService;
+import org.killbill.billing.payment.retry.PluginFailureRetryService;
+
+import com.google.inject.Inject;
+
+public class DefaultPaymentService implements PaymentService {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultPaymentService.class);
+
+ public static final String SERVICE_NAME = "payment-service";
+
+ private final InvoiceHandler invoiceHandler;
+ private final PaymentTagHandler tagHandler;
+ private final PersistentBus eventBus;
+ private final PaymentApi api;
+ private final FailedPaymentRetryService failedRetryService;
+ private final PluginFailureRetryService timedoutRetryService;
+ private final AutoPayRetryService autoPayoffRetryService;
+
+ @Inject
+ public DefaultPaymentService(final InvoiceHandler invoiceHandler,
+ final PaymentTagHandler tagHandler,
+ final PaymentApi api, final PersistentBus eventBus,
+ final FailedPaymentRetryService failedRetryService,
+ final PluginFailureRetryService timedoutRetryService,
+ final AutoPayRetryService autoPayoffRetryService) {
+ this.invoiceHandler = invoiceHandler;
+ this.tagHandler = tagHandler;
+ this.eventBus = eventBus;
+ this.api = api;
+ this.failedRetryService = failedRetryService;
+ this.timedoutRetryService = timedoutRetryService;
+ this.autoPayoffRetryService = autoPayoffRetryService;
+ }
+
+ @Override
+ public String getName() {
+ return SERVICE_NAME;
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.INIT_SERVICE)
+ public void initialize() throws NotificationQueueAlreadyExists {
+ try {
+ eventBus.register(invoiceHandler);
+ eventBus.register(tagHandler);
+ } catch (PersistentBus.EventBusException e) {
+ log.error("Unable to register with the EventBus!", e);
+ }
+ failedRetryService.initialize(SERVICE_NAME);
+ timedoutRetryService.initialize(SERVICE_NAME);
+ autoPayoffRetryService.initialize(SERVICE_NAME);
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.START_SERVICE)
+ public void start() {
+ failedRetryService.start();
+ timedoutRetryService.start();
+ autoPayoffRetryService.start();
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE)
+ public void stop() throws NoSuchNotificationQueue {
+ try {
+ eventBus.unregister(invoiceHandler);
+ eventBus.unregister(tagHandler);
+ } catch (PersistentBus.EventBusException e) {
+ throw new RuntimeException("Unable to unregister to the EventBus!", e);
+ }
+ failedRetryService.stop();
+ timedoutRetryService.stop();
+ autoPayoffRetryService.stop();
+ }
+
+ @Override
+ public PaymentApi getPaymentApi() {
+ return api;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
new file mode 100644
index 0000000..a221bbd
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.glue;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.DefaultPaymentApi;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentInternalApi;
+import org.killbill.billing.payment.api.PaymentService;
+import org.killbill.billing.payment.api.svcs.DefaultPaymentInternalApi;
+import org.killbill.billing.payment.bus.InvoiceHandler;
+import org.killbill.billing.payment.bus.PaymentTagHandler;
+import org.killbill.billing.payment.core.PaymentMethodProcessor;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.payment.core.RefundProcessor;
+import org.killbill.billing.payment.dao.DefaultPaymentDao;
+import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.retry.AutoPayRetryService;
+import org.killbill.billing.payment.retry.AutoPayRetryService.AutoPayRetryServiceScheduler;
+import org.killbill.billing.payment.retry.FailedPaymentRetryService;
+import org.killbill.billing.payment.retry.FailedPaymentRetryService.FailedPaymentRetryServiceScheduler;
+import org.killbill.billing.payment.retry.PluginFailureRetryService;
+import org.killbill.billing.payment.retry.PluginFailureRetryService.PluginFailureRetryServiceScheduler;
+import org.killbill.billing.util.config.PaymentConfig;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+
+public class PaymentModule extends AbstractModule {
+
+ private static final String PLUGIN_THREAD_PREFIX = "Plugin-th-";
+
+ public static final String PLUGIN_EXECUTOR_NAMED = "PluginExecutor";
+
+ protected ConfigSource configSource;
+
+ public PaymentModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ protected void installPaymentDao() {
+ bind(PaymentDao.class).to(DefaultPaymentDao.class).asEagerSingleton();
+ }
+
+ protected void installPaymentProviderPlugins(final PaymentConfig config) {
+ }
+
+ protected void installRetryEngines() {
+ bind(FailedPaymentRetryService.class).asEagerSingleton();
+ bind(PluginFailureRetryService.class).asEagerSingleton();
+ bind(AutoPayRetryService.class).asEagerSingleton();
+ bind(FailedPaymentRetryServiceScheduler.class).asEagerSingleton();
+ bind(PluginFailureRetryServiceScheduler.class).asEagerSingleton();
+ bind(AutoPayRetryServiceScheduler.class).asEagerSingleton();
+ }
+
+ protected void installProcessors(final PaymentConfig paymentConfig) {
+ final ExecutorService pluginExecutorService = Executors.newFixedThreadPool(paymentConfig.getPaymentPluginThreadNb(), new ThreadFactory() {
+
+ @Override
+ public Thread newThread(final Runnable r) {
+ final Thread th = new Thread(r);
+ th.setName(PLUGIN_THREAD_PREFIX + th.getId());
+ return th;
+ }
+ });
+ bind(ExecutorService.class).annotatedWith(Names.named(PLUGIN_EXECUTOR_NAMED)).toInstance(pluginExecutorService);
+ bind(PaymentProcessor.class).asEagerSingleton();
+ bind(RefundProcessor.class).asEagerSingleton();
+ bind(PaymentMethodProcessor.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ final ConfigurationObjectFactory factory = new ConfigurationObjectFactory(configSource);
+ final PaymentConfig paymentConfig = factory.build(PaymentConfig.class);
+
+ bind(PaymentConfig.class).toInstance(paymentConfig);
+ bind(new TypeLiteral<OSGIServiceRegistration<PaymentPluginApi>>() {}).toProvider(DefaultPaymentProviderPluginRegistryProvider.class).asEagerSingleton();
+
+ bind(PaymentInternalApi.class).to(DefaultPaymentInternalApi.class).asEagerSingleton();
+ bind(PaymentApi.class).to(DefaultPaymentApi.class).asEagerSingleton();
+ bind(InvoiceHandler.class).asEagerSingleton();
+ bind(PaymentTagHandler.class).asEagerSingleton();
+ bind(PaymentService.class).to(DefaultPaymentService.class).asEagerSingleton();
+ installPaymentProviderPlugins(paymentConfig);
+ installPaymentDao();
+ installProcessors(paymentConfig);
+ installRetryEngines();
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java
new file mode 100644
index 0000000..4e5aa69
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+
+public class DefaultNoOpPaymentInfoPlugin implements PaymentInfoPlugin {
+
+ private final UUID kbPaymentId;
+ private final BigDecimal amount;
+ private final DateTime effectiveDate;
+ private final DateTime createdDate;
+ private final PaymentPluginStatus status;
+ private final String error;
+ private final Currency currency;
+
+ public DefaultNoOpPaymentInfoPlugin(final UUID kbPaymentId, final BigDecimal amount, final Currency currency, final DateTime effectiveDate,
+ final DateTime createdDate, final PaymentPluginStatus status, final String error) {
+ this.kbPaymentId = kbPaymentId;
+ this.amount = amount;
+ this.effectiveDate = effectiveDate;
+ this.createdDate = createdDate;
+ this.status = status;
+ this.error = error;
+ this.currency = currency;
+ }
+
+ @Override
+ public UUID getKbPaymentId() {
+ return kbPaymentId;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public PaymentPluginStatus getStatus() {
+ return status;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return createdDate;
+ }
+
+ @Override
+ public String getGatewayError() {
+ return error;
+ }
+
+ @Override
+ public String getGatewayErrorCode() {
+ return null;
+ }
+
+ @Override
+ public String getFirstPaymentReferenceId() {
+ return null;
+ }
+
+ @Override
+ public String getSecondPaymentReferenceId() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultNoOpPaymentInfoPlugin{");
+ sb.append("kbPaymentId=").append(kbPaymentId);
+ sb.append(", amount=").append(amount);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", createdDate=").append(createdDate);
+ sb.append(", status=").append(status);
+ sb.append(", error='").append(error).append('\'');
+ sb.append(", currency=").append(currency);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultNoOpPaymentInfoPlugin that = (DefaultNoOpPaymentInfoPlugin) o;
+
+ if (amount != null ? amount.compareTo(that.amount) != 0 : that.amount != null) {
+ return false;
+ }
+ if (createdDate != null ? createdDate.compareTo(that.createdDate) != 0 : that.createdDate != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (effectiveDate != null ? effectiveDate.compareTo(that.effectiveDate) != 0 : that.effectiveDate != null) {
+ return false;
+ }
+ if (error != null ? !error.equals(that.error) : that.error != null) {
+ return false;
+ }
+ if (kbPaymentId != null ? !kbPaymentId.equals(that.kbPaymentId) : that.kbPaymentId != null) {
+ return false;
+ }
+ if (status != that.status) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = kbPaymentId != null ? kbPaymentId.hashCode() : 0;
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+ result = 31 * result + (status != null ? status.hashCode() : 0);
+ result = 31 * result + (error != null ? error.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java
new file mode 100644
index 0000000..f22baa9
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.payment.api.PaymentMethodKVInfo;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+
+public class DefaultNoOpPaymentMethodPlugin implements PaymentMethodPlugin {
+
+ private final UUID kbPaymentMethodId;
+ private final String externalId;
+ private final boolean isDefault;
+ private List<PaymentMethodKVInfo> props;
+
+ public DefaultNoOpPaymentMethodPlugin(final UUID kbPaymentMethodId, final PaymentMethodPlugin src) {
+ this.kbPaymentMethodId = kbPaymentMethodId;
+ this.externalId = UUID.randomUUID().toString();
+ this.isDefault = src.isDefaultPaymentMethod();
+ this.props = src.getProperties();
+ }
+
+ public DefaultNoOpPaymentMethodPlugin(final String externalId,
+ final boolean isDefault,
+ final List<PaymentMethodKVInfo> props) {
+ this(null, externalId, isDefault, props);
+ }
+
+ public DefaultNoOpPaymentMethodPlugin(@Nullable final UUID kbPaymentMethodId,
+ final String externalId,
+ final boolean isDefault,
+ final List<PaymentMethodKVInfo> props) {
+ this.kbPaymentMethodId = kbPaymentMethodId;
+ this.externalId = externalId;
+ this.isDefault = isDefault;
+ this.props = props;
+ }
+
+ @Override
+ public UUID getKbPaymentMethodId() {
+ return kbPaymentMethodId;
+ }
+
+ @Override
+ public String getExternalPaymentMethodId() {
+ return externalId;
+ }
+
+ @Override
+ public boolean isDefaultPaymentMethod() {
+ return isDefault;
+ }
+
+ @Override
+ public List<PaymentMethodKVInfo> getProperties() {
+ return props;
+ }
+
+ public void setProps(final List<PaymentMethodKVInfo> props) {
+ this.props = props;
+ }
+
+ @Override
+ public String getType() {
+ return "noop";
+ }
+
+ @Override
+ public String getCCName() {
+ return null;
+ }
+
+ @Override
+ public String getCCType() {
+ return null;
+ }
+
+ @Override
+ public String getCCExpirationMonth() {
+ return null;
+ }
+
+ @Override
+ public String getCCExpirationYear() {
+ return null;
+ }
+
+ @Override
+ public String getCCLast4() {
+ return null;
+ }
+
+ @Override
+ public String getAddress1() {
+ return null;
+ }
+
+ @Override
+ public String getAddress2() {
+ return null;
+ }
+
+ @Override
+ public String getCity() {
+ return null;
+ }
+
+ @Override
+ public String getState() {
+ return null;
+ }
+
+ @Override
+ public String getZip() {
+ return null;
+ }
+
+ @Override
+ public String getCountry() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultNoOpPaymentMethodPlugin");
+ sb.append("{externalId='").append(externalId).append('\'');
+ sb.append(", isDefault=").append(isDefault);
+ sb.append(", props=").append(props);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultNoOpPaymentMethodPlugin that = (DefaultNoOpPaymentMethodPlugin) o;
+
+ if (isDefault != that.isDefault) {
+ return false;
+ }
+ if (externalId != null ? !externalId.equals(that.externalId) : that.externalId != null) {
+ return false;
+ }
+ if (props != null ? !props.equals(that.props) : that.props != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = externalId != null ? externalId.hashCode() : 0;
+ result = 31 * result + (isDefault ? 1 : 0);
+ result = 31 * result + (props != null ? props.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
new file mode 100644
index 0000000..fd1deed
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.math.BigDecimal;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.clock.Clock;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+import org.killbill.billing.payment.plugin.api.NoOpPaymentPluginApi;
+import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.plugin.api.RefundInfoPlugin;
+import org.killbill.billing.payment.plugin.api.RefundPluginStatus;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.DefaultPagination;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
+
+public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
+
+ private static final String PLUGIN_NAME = "__NO_OP__";
+
+ private final AtomicBoolean makeNextInvoiceFailWithError = new AtomicBoolean(false);
+ private final AtomicBoolean makeNextInvoiceFailWithException = new AtomicBoolean(false);
+ private final AtomicBoolean makeAllInvoicesFailWithError = new AtomicBoolean(false);
+
+ private final Map<String, PaymentInfoPlugin> payments = new ConcurrentHashMap<String, PaymentInfoPlugin>();
+ // Note: we can't use HashMultiMap as we care about storing duplicate key/value pairs
+ private final Multimap<String, RefundInfoPlugin> refunds = LinkedListMultimap.<String, RefundInfoPlugin>create();
+ private final Map<String, List<PaymentMethodPlugin>> paymentMethods = new ConcurrentHashMap<String, List<PaymentMethodPlugin>>();
+
+ private final Clock clock;
+
+ @Inject
+ public DefaultNoOpPaymentProviderPlugin(final Clock clock) {
+ this.clock = clock;
+ clear();
+ }
+
+ @Override
+ public void clear() {
+ makeNextInvoiceFailWithException.set(false);
+ makeAllInvoicesFailWithError.set(false);
+ makeNextInvoiceFailWithError.set(false);
+ }
+
+ @Override
+ public void makeNextPaymentFailWithError() {
+ makeNextInvoiceFailWithError.set(true);
+ }
+
+ @Override
+ public void makeNextPaymentFailWithException() {
+ makeNextInvoiceFailWithException.set(true);
+ }
+
+ @Override
+ public void makeAllInvoicesFailWithError(final boolean failure) {
+ makeAllInvoicesFailWithError.set(failure);
+ }
+
+ @Override
+ public PaymentInfoPlugin processPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+ if (makeNextInvoiceFailWithException.getAndSet(false)) {
+ throw new PaymentPluginApiException("", "test error");
+ }
+
+ final PaymentPluginStatus status = (makeAllInvoicesFailWithError.get() || makeNextInvoiceFailWithError.getAndSet(false)) ? PaymentPluginStatus.ERROR : PaymentPluginStatus.PROCESSED;
+ final PaymentInfoPlugin result = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, amount, currency, clock.getUTCNow(), clock.getUTCNow(), status, null);
+ payments.put(kbPaymentId.toString(), result);
+ return result;
+ }
+
+ @Override
+ public PaymentInfoPlugin getPaymentInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) throws PaymentPluginApiException {
+ final PaymentInfoPlugin payment = payments.get(kbPaymentId.toString());
+ if (payment == null) {
+ throw new PaymentPluginApiException("", "No payment found for payment id " + kbPaymentId.toString());
+ }
+ return payment;
+ }
+
+ @Override
+ public Pagination<PaymentInfoPlugin> searchPayments(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ final ImmutableList<PaymentInfoPlugin> allResults = ImmutableList.<PaymentInfoPlugin>copyOf(Iterables.<PaymentInfoPlugin>filter(Iterables.<PaymentInfoPlugin>concat(payments.values()), new Predicate<PaymentInfoPlugin>() {
+ @Override
+ public boolean apply(final PaymentInfoPlugin input) {
+ return (input.getKbPaymentId() != null && input.getKbPaymentId().toString().equals(searchKey)) ||
+ (input.getFirstPaymentReferenceId() != null && input.getFirstPaymentReferenceId().contains(searchKey)) ||
+ (input.getSecondPaymentReferenceId() != null && input.getSecondPaymentReferenceId().contains(searchKey));
+ }
+ }));
+
+ final List<PaymentInfoPlugin> results;
+ if (offset >= allResults.size()) {
+ results = ImmutableList.<PaymentInfoPlugin>of();
+ } else if (offset + limit > allResults.size()) {
+ results = allResults.subList(offset.intValue(), allResults.size());
+ } else {
+ results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue());
+ }
+
+ return new DefaultPagination<PaymentInfoPlugin>(offset, limit, (long) results.size(), (long) payments.values().size(), results.iterator());
+ }
+
+ @Override
+ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final CallContext context) throws PaymentPluginApiException {
+ final PaymentMethodPlugin realWithID = new DefaultNoOpPaymentMethodPlugin(kbPaymentMethodId, paymentMethodProps);
+ List<PaymentMethodPlugin> pms = paymentMethods.get(kbPaymentMethodId.toString());
+ if (pms == null) {
+ pms = new LinkedList<PaymentMethodPlugin>();
+ paymentMethods.put(kbPaymentMethodId.toString(), pms);
+ }
+ pms.add(realWithID);
+ }
+
+ @Override
+ public void deletePaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+ PaymentMethodPlugin toBeDeleted = null;
+ final List<PaymentMethodPlugin> pms = paymentMethods.get(kbPaymentMethodId.toString());
+ if (pms != null) {
+ for (final PaymentMethodPlugin cur : pms) {
+ if (cur.getExternalPaymentMethodId().equals(kbPaymentMethodId.toString())) {
+ toBeDeleted = cur;
+ break;
+ }
+ }
+ }
+
+ if (toBeDeleted != null) {
+ pms.remove(toBeDeleted);
+ }
+ }
+
+ @Override
+ public PaymentMethodPlugin getPaymentMethodDetail(final UUID kbAccountId, final UUID kbPaymentMethodId, final TenantContext context) throws PaymentPluginApiException {
+ final List<PaymentMethodPlugin> paymentMethodPlugins = paymentMethods.get(kbPaymentMethodId.toString());
+ if (paymentMethodPlugins == null || paymentMethodPlugins.size() == 0) {
+ return null;
+ } else {
+ return paymentMethodPlugins.get(0);
+ }
+ }
+
+ @Override
+ public void setDefaultPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public List<PaymentMethodInfoPlugin> getPaymentMethods(final UUID kbAccountId, final boolean refreshFromGateway, final CallContext context) {
+ return ImmutableList.<PaymentMethodInfoPlugin>of();
+ }
+
+ @Override
+ public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ final ImmutableList<PaymentMethodPlugin> allResults = ImmutableList.<PaymentMethodPlugin>copyOf(Iterables.<PaymentMethodPlugin>filter(Iterables.<PaymentMethodPlugin>concat(paymentMethods.values()), new Predicate<PaymentMethodPlugin>() {
+ @Override
+ public boolean apply(final PaymentMethodPlugin input) {
+ return (input.getAddress1() != null && input.getAddress1().contains(searchKey)) ||
+ (input.getAddress2() != null && input.getAddress2().contains(searchKey)) ||
+ (input.getCCLast4() != null && input.getCCLast4().contains(searchKey)) ||
+ (input.getCCName() != null && input.getCCName().contains(searchKey)) ||
+ (input.getCity() != null && input.getCity().contains(searchKey)) ||
+ (input.getState() != null && input.getState().contains(searchKey)) ||
+ (input.getCountry() != null && input.getCountry().contains(searchKey));
+ }
+ }));
+
+ final List<PaymentMethodPlugin> results;
+ if (offset >= allResults.size()) {
+ results = ImmutableList.<PaymentMethodPlugin>of();
+ } else if (offset + limit > allResults.size()) {
+ results = allResults.subList(offset.intValue(), allResults.size());
+ } else {
+ results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue());
+ }
+
+ return new DefaultPagination<PaymentMethodPlugin>(offset, limit, (long) results.size(), (long) paymentMethods.values().size(), results.iterator());
+ }
+
+ @Override
+ public void resetPaymentMethods(final UUID kbAccountId, final List<PaymentMethodInfoPlugin> paymentMethods) {
+ }
+
+ @Override
+ public RefundInfoPlugin processRefund(final UUID kbAccountId, final UUID kbPaymentId, final BigDecimal refundAmount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+ final PaymentInfoPlugin paymentInfoPlugin = getPaymentInfo(kbAccountId, kbPaymentId, context);
+ if (paymentInfoPlugin == null) {
+ throw new PaymentPluginApiException("", String.format("No payment found for payment id %s (plugin %s)", kbPaymentId.toString(), PLUGIN_NAME));
+ }
+
+ BigDecimal maxAmountRefundable = paymentInfoPlugin.getAmount();
+ for (final RefundInfoPlugin refund : refunds.get(kbPaymentId.toString())) {
+ maxAmountRefundable = maxAmountRefundable.add(refund.getAmount().negate());
+ }
+ if (maxAmountRefundable.compareTo(refundAmount) < 0) {
+ throw new PaymentPluginApiException("", String.format("Refund amount of %s for payment id %s is bigger than the payment amount %s (plugin %s)",
+ refundAmount, kbPaymentId.toString(), paymentInfoPlugin.getAmount(), PLUGIN_NAME));
+ }
+
+ final DefaultNoOpRefundInfoPlugin refundInfoPlugin = new DefaultNoOpRefundInfoPlugin(kbPaymentId, refundAmount, currency, clock.getUTCNow(), clock.getUTCNow(), RefundPluginStatus.PROCESSED, null);
+ refunds.put(kbPaymentId.toString(), refundInfoPlugin);
+
+ return refundInfoPlugin;
+ }
+
+ @Override
+ public List<RefundInfoPlugin> getRefundInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) {
+ return ImmutableList.<RefundInfoPlugin>copyOf(refunds.get(kbPaymentId.toString()));
+ }
+
+ @Override
+ public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ final ImmutableList<RefundInfoPlugin> allResults = ImmutableList.<RefundInfoPlugin>copyOf(Iterables.<RefundInfoPlugin>filter(Iterables.<RefundInfoPlugin>concat(refunds.values()), new Predicate<RefundInfoPlugin>() {
+ @Override
+ public boolean apply(final RefundInfoPlugin input) {
+ return (input.getFirstRefundReferenceId() != null && input.getFirstRefundReferenceId().contains(searchKey)) ||
+ (input.getSecondRefundReferenceId() != null && input.getSecondRefundReferenceId().contains(searchKey));
+ }
+ }));
+
+ final List<RefundInfoPlugin> results;
+ if (offset >= allResults.size()) {
+ results = ImmutableList.<RefundInfoPlugin>of();
+ } else if (offset + limit > allResults.size()) {
+ results = allResults.subList(offset.intValue(), allResults.size());
+ } else {
+ results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue());
+ }
+
+ return new DefaultPagination<RefundInfoPlugin>(offset, limit, (long) results.size(), (long) refunds.values().size(), results.iterator());
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpRefundInfoPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpRefundInfoPlugin.java
new file mode 100644
index 0000000..66c4145
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpRefundInfoPlugin.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.plugin.api.RefundInfoPlugin;
+import org.killbill.billing.payment.plugin.api.RefundPluginStatus;
+
+public class DefaultNoOpRefundInfoPlugin implements RefundInfoPlugin {
+
+ private final UUID kbPaymentId;
+ private final BigDecimal amount;
+ private final Currency currency;
+ private final DateTime effectiveDate;
+ private final DateTime createdDate;
+ private final RefundPluginStatus status;
+ private final String error;
+
+ public DefaultNoOpRefundInfoPlugin(final UUID kbPaymentId, final BigDecimal amount, final Currency currency, final DateTime effectiveDate,
+ final DateTime createdDate, final RefundPluginStatus status, final String error) {
+ this.kbPaymentId = kbPaymentId;
+ this.amount = amount;
+ this.currency = currency;
+ this.effectiveDate = effectiveDate;
+ this.createdDate = createdDate;
+ this.status = status;
+ this.error = error;
+ }
+
+ @Override
+ public UUID getKbPaymentId() {
+ return kbPaymentId;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public RefundPluginStatus getStatus() {
+ return status;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return createdDate;
+ }
+
+ @Override
+ public String getGatewayError() {
+ return error;
+ }
+
+ @Override
+ public String getGatewayErrorCode() {
+ return null;
+ }
+
+ @Override
+ public String getFirstRefundReferenceId() {
+ return null;
+ }
+
+ @Override
+ public String getSecondRefundReferenceId() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultNoOpRefundInfoPlugin{");
+ sb.append("kbPaymentId=").append(kbPaymentId);
+ sb.append(", amount=").append(amount);
+ sb.append(", currency=").append(currency);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", createdDate=").append(createdDate);
+ sb.append(", status=").append(status);
+ sb.append(", error='").append(error).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultNoOpRefundInfoPlugin that = (DefaultNoOpRefundInfoPlugin) o;
+
+ if (amount != null ? amount.compareTo(that.amount) != 0 : that.amount != null) {
+ return false;
+ }
+ if (createdDate != null ? createdDate.compareTo(that.createdDate) != 0 : that.createdDate != null) {
+ return false;
+ }
+ if (currency != that.currency) {
+ return false;
+ }
+ if (effectiveDate != null ? effectiveDate.compareTo(that.effectiveDate) != 0 : that.effectiveDate != null) {
+ return false;
+ }
+ if (error != null ? !error.equals(that.error) : that.error != null) {
+ return false;
+ }
+ if (kbPaymentId != null ? !kbPaymentId.equals(that.kbPaymentId) : that.kbPaymentId != null) {
+ return false;
+ }
+ if (status != that.status) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = kbPaymentId != null ? kbPaymentId.hashCode() : 0;
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ result = 31 * result + (currency != null ? currency.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+ result = 31 * result + (status != null ? status.hashCode() : 0);
+ result = 31 * result + (error != null ? error.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentMethodInfoPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentMethodInfoPlugin.java
new file mode 100644
index 0000000..d9f8c5e
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentMethodInfoPlugin.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.util.UUID;
+
+import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
+
+public class DefaultPaymentMethodInfoPlugin implements PaymentMethodInfoPlugin {
+
+ private final UUID accountId;
+ private final UUID paymentMethodId;
+ private final boolean isDefault;
+ private final String externalPaymentMethodId;
+
+ public DefaultPaymentMethodInfoPlugin(final UUID accountId, final UUID paymentMethodId, final boolean aDefault, final String externalPaymentMethodId) {
+ this.accountId = accountId;
+ this.paymentMethodId = paymentMethodId;
+ isDefault = aDefault;
+ this.externalPaymentMethodId = externalPaymentMethodId;
+ }
+
+ public DefaultPaymentMethodInfoPlugin(PaymentMethodInfoPlugin input, final UUID paymentMethodId) {
+ this(input.getAccountId(), paymentMethodId, input.isDefault(), input.getExternalPaymentMethodId());
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public UUID getPaymentMethodId() {
+ return paymentMethodId;
+ }
+
+ @Override
+ public boolean isDefault() {
+ return isDefault;
+ }
+
+ @Override
+ public String getExternalPaymentMethodId() {
+ return externalPaymentMethodId;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java
new file mode 100644
index 0000000..5d9c44e
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.util.config.PaymentConfig;
+
+import com.google.inject.Inject;
+
+public class DefaultPaymentProviderPluginRegistry implements OSGIServiceRegistration<PaymentPluginApi> {
+
+ private final static Logger log = LoggerFactory.getLogger(DefaultPaymentProviderPluginRegistry.class);
+
+ private final String defaultPlugin;
+ private final Map<String, PaymentPluginApi> pluginsByName = new ConcurrentHashMap<String, PaymentPluginApi>();
+
+ @Inject
+ public DefaultPaymentProviderPluginRegistry(final PaymentConfig config) {
+ this.defaultPlugin = config.getDefaultPaymentProvider();
+ }
+
+
+ @Override
+ public void registerService(final OSGIServiceDescriptor desc, final PaymentPluginApi service) {
+ log.info("DefaultPaymentProviderPluginRegistry registering service " + desc.getRegistrationName());
+ pluginsByName.put(desc.getRegistrationName(), service);
+ }
+
+ @Override
+ public void unregisterService(final String serviceName) {
+ log.info("DefaultPaymentProviderPluginRegistry unregistering service " + serviceName);
+ pluginsByName.remove(serviceName);
+ }
+
+ @Override
+ public PaymentPluginApi getServiceForName(final String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null payment plugin APi name");
+ }
+ final PaymentPluginApi plugin = pluginsByName.get(name);
+ return plugin;
+ }
+
+ @Override
+ public Set<String> getAllServices() {
+ return pluginsByName.keySet();
+ }
+
+ @Override
+ public Class<PaymentPluginApi> getServiceType() {
+ return PaymentPluginApi.class;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java
new file mode 100644
index 0000000..6c5579a
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.clock.Clock;
+import org.killbill.billing.payment.api.PaymentMethodKVInfo;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.plugin.api.RefundInfoPlugin;
+import org.killbill.billing.payment.plugin.api.RefundPluginStatus;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.DefaultPagination;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+import com.google.inject.Inject;
+
+/**
+ * Special plugin used to record external payments (i.e. payments not issued by Killbill), such as checks.
+ * <p/>
+ * The implementation is very similar to the no-op plugin, which it extends. This can potentially be an issue
+ * if Killbill is processing a lot of external payments as they are all kept in memory.
+ */
+public class ExternalPaymentProviderPlugin implements PaymentPluginApi {
+
+ public static final String PLUGIN_NAME = "__EXTERNAL_PAYMENT__";
+
+ private final Clock clock;
+
+ @Inject
+ public ExternalPaymentProviderPlugin(final Clock clock) {
+ this.clock = clock;
+ }
+
+ @Override
+ public PaymentInfoPlugin processPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+ return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, amount, currency, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null);
+ }
+
+ @Override
+ public PaymentInfoPlugin getPaymentInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) throws PaymentPluginApiException {
+ return new DefaultNoOpPaymentInfoPlugin(kbPaymentId, BigDecimal.ZERO, null, clock.getUTCNow(), clock.getUTCNow(), PaymentPluginStatus.PROCESSED, null);
+ }
+
+ @Override
+ public Pagination<PaymentInfoPlugin> searchPayments(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return new DefaultPagination<PaymentInfoPlugin>(offset, limit, 0L, 0L, Iterators.<PaymentInfoPlugin>emptyIterator());
+ }
+
+ @Override
+ public RefundInfoPlugin processRefund(final UUID kbAccountId, final UUID kbPaymentId, final BigDecimal refundAmount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+ return new DefaultNoOpRefundInfoPlugin(kbPaymentId, BigDecimal.ZERO, currency, clock.getUTCNow(), clock.getUTCNow(), RefundPluginStatus.PROCESSED, null);
+ }
+
+ @Override
+ public List<RefundInfoPlugin> getRefundInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) throws PaymentPluginApiException {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return new DefaultPagination<RefundInfoPlugin>(offset, limit, 0L, 0L, Iterators.<RefundInfoPlugin>emptyIterator());
+ }
+
+ @Override
+ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public void deletePaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public PaymentMethodPlugin getPaymentMethodDetail(final UUID kbAccountId, final UUID kbPaymentMethodId, final TenantContext context) throws PaymentPluginApiException {
+ return new DefaultNoOpPaymentMethodPlugin(kbPaymentMethodId, "unknown", false, Collections.<PaymentMethodKVInfo>emptyList());
+ }
+
+ @Override
+ public void setDefaultPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public List<PaymentMethodInfoPlugin> getPaymentMethods(final UUID kbAccountId, final boolean refreshFromGateway, final CallContext context) throws PaymentPluginApiException {
+ return ImmutableList.<PaymentMethodInfoPlugin>of();
+ }
+
+ @Override
+ public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ return new DefaultPagination<PaymentMethodPlugin>(offset, limit, 0L, 0L, Iterators.<PaymentMethodPlugin>emptyIterator());
+ }
+
+ @Override
+ public void resetPaymentMethods(final UUID kbAccountId, final List<PaymentMethodInfoPlugin> paymentMethods) throws PaymentPluginApiException {
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginModule.java b/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginModule.java
new file mode 100644
index 0000000..94b2155
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginModule.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+public class NoOpPaymentProviderPluginModule extends AbstractModule {
+ private final String instanceName;
+
+ public NoOpPaymentProviderPluginModule(final String instanceName) {
+ this.instanceName = instanceName;
+ }
+
+ @Override
+ protected void configure() {
+ bind(DefaultNoOpPaymentProviderPlugin.class)
+ .annotatedWith(Names.named(instanceName))
+ .toProvider(new NoOpPaymentProviderPluginProvider(instanceName))
+ .asEagerSingleton();
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginProvider.java b/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginProvider.java
new file mode 100644
index 0000000..8b687c0
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/NoOpPaymentProviderPluginProvider.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.clock.Clock;
+
+public class NoOpPaymentProviderPluginProvider implements Provider<DefaultNoOpPaymentProviderPlugin> {
+
+ private final String instanceName;
+
+ private Clock clock;
+ private OSGIServiceRegistration<PaymentPluginApi> registry;
+
+ public NoOpPaymentProviderPluginProvider(final String instanceName) {
+ this.instanceName = instanceName;
+
+ }
+
+ @Inject
+ public void setPaymentProviderPluginRegistry(final OSGIServiceRegistration<PaymentPluginApi> registry, final Clock clock) {
+ this.clock = clock;
+ this.registry = registry;
+ }
+
+ @Override
+ public DefaultNoOpPaymentProviderPlugin get() {
+
+ final DefaultNoOpPaymentProviderPlugin plugin = new DefaultNoOpPaymentProviderPlugin(clock);
+ final OSGIServiceDescriptor desc = new OSGIServiceDescriptor() {
+ @Override
+ public String getPluginSymbolicName() {
+ return null;
+ }
+ @Override
+ public String getRegistrationName() {
+ return instanceName;
+ }
+ };
+ registry.registerService(desc, plugin);
+ return plugin;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/AutoPayRetryService.java b/payment/src/main/java/org/killbill/billing/payment/retry/AutoPayRetryService.java
new file mode 100644
index 0000000..0686db9
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/AutoPayRetryService.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.retry;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.util.config.PaymentConfig;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+
+import com.google.inject.Inject;
+
+public class AutoPayRetryService extends BaseRetryService implements RetryService {
+
+ public static final String QUEUE_NAME = "autopayoff";
+
+ private final PaymentProcessor paymentProcessor;
+
+ @Inject
+ public AutoPayRetryService(final NotificationQueueService notificationQueueService,
+ final PaymentConfig config,
+ final PaymentProcessor paymentProcessor,
+ final InternalCallContextFactory internalCallContextFactory) {
+ super(notificationQueueService, internalCallContextFactory);
+ this.paymentProcessor = paymentProcessor;
+ }
+
+ @Override
+ public String getQueueName() {
+ return QUEUE_NAME;
+ }
+
+ @Override
+ public void retry(final UUID paymentId, final InternalCallContext context) {
+ paymentProcessor.retryAutoPayOff(paymentId, context);
+ }
+
+ public static class AutoPayRetryServiceScheduler extends RetryServiceScheduler {
+
+ @Inject
+ public AutoPayRetryServiceScheduler(final NotificationQueueService notificationQueueService,
+ final InternalCallContextFactory internalCallContextFactory) {
+ super(notificationQueueService, internalCallContextFactory);
+ }
+
+ @Override
+ public boolean scheduleRetry(final UUID paymentId, final DateTime timeOfRetry) {
+ return super.scheduleRetry(paymentId, timeOfRetry);
+ }
+
+ @Override
+ public String getQueueName() {
+ return QUEUE_NAME;
+ }
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java b/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java
new file mode 100644
index 0000000..2aa7e45
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/BaseRetryService.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.retry;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueHandler;
+import org.killbill.billing.payment.glue.DefaultPaymentService;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.inject.Inject;
+
+public abstract class BaseRetryService implements RetryService {
+
+ private static final Logger log = LoggerFactory.getLogger(BaseRetryService.class);
+ private static final String PAYMENT_RETRY_SERVICE = "PaymentRetryService";
+
+ private final NotificationQueueService notificationQueueService;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ private NotificationQueue retryQueue;
+
+ public BaseRetryService(final NotificationQueueService notificationQueueService,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.notificationQueueService = notificationQueueService;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public void initialize(final String svcName) throws NotificationQueueAlreadyExists {
+ retryQueue = notificationQueueService.createNotificationQueue(svcName,
+ getQueueName(),
+ new NotificationQueueHandler() {
+ @Override
+ public void handleReadyNotification(final NotificationEvent notificationKey, final DateTime eventDateTime, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ if (!(notificationKey instanceof PaymentRetryNotificationKey)) {
+ log.error("Payment service got an unexpected notification type {}", notificationKey.getClass().getName());
+ return;
+ }
+ final PaymentRetryNotificationKey key = (PaymentRetryNotificationKey) notificationKey;
+ final InternalCallContext callContext = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, PAYMENT_RETRY_SERVICE, CallOrigin.INTERNAL, UserType.SYSTEM, userToken);
+ retry(key.getUuidKey(), callContext);
+ }
+ });
+ }
+
+ @Override
+ public void start() {
+ retryQueue.startQueue();
+ }
+
+ @Override
+ public void stop() throws NoSuchNotificationQueue {
+ if (retryQueue != null) {
+ retryQueue.stopQueue();
+ notificationQueueService.deleteNotificationQueue(retryQueue.getServiceName(), retryQueue.getQueueName());
+ }
+ }
+
+ @Override
+ public abstract String getQueueName();
+
+ public abstract static class RetryServiceScheduler {
+
+ private final NotificationQueueService notificationQueueService;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public RetryServiceScheduler(final NotificationQueueService notificationQueueService,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.notificationQueueService = notificationQueueService;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ public boolean scheduleRetryFromTransaction(final UUID paymentId, final DateTime timeOfRetry, final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) {
+ return scheduleRetryInternal(paymentId, timeOfRetry, entitySqlDaoWrapperFactory);
+ }
+
+ public boolean scheduleRetry(final UUID paymentId, final DateTime timeOfRetry) {
+ return scheduleRetryInternal(paymentId, timeOfRetry, null);
+ }
+
+ // STEPH TimedoutPaymentRetryServiceScheduler
+ public void cancelAllScheduleRetryForKey(final UUID paymentId) {
+ /*
+ try {
+ NotificationQueue retryQueue = notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, getQueueName());
+ NotificationKey key = new NotificationKey() {
+ @Override
+ public String toString() {
+ return paymentId.toString();
+ }
+ };
+ retryQueue.removeNotificationsByKey(key);
+ } catch (NoSuchNotificationQueue e) {
+ log.error(String.format("Failed to retrieve notification queue %s:%s", DefaultPaymentService.SERVICE_NAME, getQueueName()));
+ }
+ */
+ }
+
+ private boolean scheduleRetryInternal(final UUID paymentId, final DateTime timeOfRetry, final EntitySqlDaoWrapperFactory<EntitySqlDao> transactionalDao) {
+ final InternalCallContext context = createCallContextFromPaymentId(paymentId);
+
+ try {
+ final NotificationQueue retryQueue = notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, getQueueName());
+ final NotificationEvent key = new PaymentRetryNotificationKey(paymentId);
+ if (retryQueue != null) {
+ if (transactionalDao == null) {
+ retryQueue.recordFutureNotification(timeOfRetry, key, context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
+ } else {
+ retryQueue.recordFutureNotificationFromTransaction(transactionalDao.getSqlDao(), timeOfRetry, key, context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
+ }
+ }
+ } catch (NoSuchNotificationQueue e) {
+ log.error(String.format("Failed to retrieve notification queue %s:%s", DefaultPaymentService.SERVICE_NAME, getQueueName()));
+ return false;
+ } catch (IOException e) {
+ log.error(String.format("Failed to serialize notificationQueue event for paymentId %s", paymentId));
+ return false;
+ }
+ return true;
+ }
+
+ protected InternalCallContext createCallContextFromPaymentId(final UUID paymentId) {
+ return internalCallContextFactory.createInternalCallContext(paymentId, ObjectType.PAYMENT, PAYMENT_RETRY_SERVICE, CallOrigin.INTERNAL, UserType.SYSTEM, null);
+ }
+
+ public abstract String getQueueName();
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/FailedPaymentRetryService.java b/payment/src/main/java/org/killbill/billing/payment/retry/FailedPaymentRetryService.java
new file mode 100644
index 0000000..18c7f36
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/FailedPaymentRetryService.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.retry;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.util.config.PaymentConfig;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.Clock;
+
+import com.google.inject.Inject;
+
+public class FailedPaymentRetryService extends BaseRetryService implements RetryService {
+
+ private static final Logger log = LoggerFactory.getLogger(FailedPaymentRetryService.class);
+
+ public static final String QUEUE_NAME = "failed-payment";
+
+ private final PaymentProcessor paymentProcessor;
+
+ @Inject
+ public FailedPaymentRetryService(final NotificationQueueService notificationQueueService,
+ final PaymentConfig config,
+ final PaymentProcessor paymentProcessor,
+ final InternalCallContextFactory internalCallContextFactory) {
+ super(notificationQueueService, internalCallContextFactory);
+ this.paymentProcessor = paymentProcessor;
+ }
+
+ @Override
+ public void retry(final UUID paymentId, final InternalCallContext context) {
+ paymentProcessor.retryFailedPayment(paymentId, context);
+ }
+
+ public static class FailedPaymentRetryServiceScheduler extends RetryServiceScheduler {
+
+ private final PaymentConfig config;
+ private final Clock clock;
+
+ @Inject
+ public FailedPaymentRetryServiceScheduler(final NotificationQueueService notificationQueueService,
+ final InternalCallContextFactory internalCallContextFactory,
+ final Clock clock,
+ final PaymentConfig config) {
+ super(notificationQueueService, internalCallContextFactory);
+ this.config = config;
+ this.clock = clock;
+ }
+
+ public boolean scheduleRetry(final UUID paymentId, final int retryAttempt) {
+ final DateTime timeOfRetry = getNextRetryDate(retryAttempt);
+ if (timeOfRetry == null) {
+ return false;
+ }
+ return super.scheduleRetry(paymentId, timeOfRetry);
+ }
+
+ private DateTime getNextRetryDate(final int retryAttempt) {
+
+ DateTime result = null;
+ final List<Integer> retryDays = config.getPaymentRetryDays();
+ final int retryCount = retryAttempt - 1;
+ if (retryCount < retryDays.size()) {
+ int retryInDays = 0;
+ final DateTime nextRetryDate = clock.getUTCNow();
+ try {
+ retryInDays = retryDays.get(retryCount);
+ result = nextRetryDate.plusDays(retryInDays);
+ } catch (NumberFormatException ex) {
+ log.error("Could not get retry day for retry count {}", retryCount);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public String getQueueName() {
+ return QUEUE_NAME;
+ }
+ }
+
+ @Override
+ public String getQueueName() {
+ return QUEUE_NAME;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/PaymentRetryNotificationKey.java b/payment/src/main/java/org/killbill/billing/payment/retry/PaymentRetryNotificationKey.java
new file mode 100644
index 0000000..00348cf
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/PaymentRetryNotificationKey.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.payment.retry;
+
+import java.util.UUID;
+
+import org.killbill.notificationq.DefaultUUIDNotificationKey;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PaymentRetryNotificationKey extends DefaultUUIDNotificationKey {
+
+ @JsonCreator
+ public PaymentRetryNotificationKey(@JsonProperty("uuidKey") UUID uuidKey) {
+ super(uuidKey);
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/PluginFailureRetryService.java b/payment/src/main/java/org/killbill/billing/payment/retry/PluginFailureRetryService.java
new file mode 100644
index 0000000..93e4765
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/PluginFailureRetryService.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.retry;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.Clock;
+import org.killbill.billing.util.config.PaymentConfig;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.inject.Inject;
+
+public class PluginFailureRetryService extends BaseRetryService implements RetryService {
+
+ private static final Logger log = LoggerFactory.getLogger(PluginFailureRetryService.class);
+
+ public static final String QUEUE_NAME = "plugin-failure";
+
+ private final PaymentProcessor paymentProcessor;
+
+ @Inject
+ public PluginFailureRetryService(final NotificationQueueService notificationQueueService,
+ final PaymentProcessor paymentProcessor,
+ final InternalCallContextFactory internalCallContextFactory) {
+ super(notificationQueueService, internalCallContextFactory);
+ this.paymentProcessor = paymentProcessor;
+ }
+
+ @Override
+ public void retry(final UUID paymentId, final InternalCallContext context) {
+ paymentProcessor.retryPluginFailure(paymentId, context);
+ }
+
+ public static class PluginFailureRetryServiceScheduler extends RetryServiceScheduler {
+
+ private final Clock clock;
+ private final PaymentConfig config;
+
+ @Inject
+ public PluginFailureRetryServiceScheduler(final NotificationQueueService notificationQueueService,
+ final InternalCallContextFactory internalCallContextFactory,
+ final Clock clock,
+ final PaymentConfig config) {
+ super(notificationQueueService, internalCallContextFactory);
+ this.clock = clock;
+ this.config = config;
+ }
+
+ @Override
+ public String getQueueName() {
+ return QUEUE_NAME;
+ }
+
+ public boolean scheduleRetry(final UUID paymentId, final int retryAttempt) {
+ final DateTime nextRetryDate = getNextRetryDate(retryAttempt);
+ if (nextRetryDate == null) {
+ return false;
+ }
+ return super.scheduleRetry(paymentId, nextRetryDate);
+ }
+
+ public boolean scheduleRetryFromTransaction(final UUID paymentId, final int retryAttempt, final EntitySqlDaoWrapperFactory<EntitySqlDao> transactionalDao) {
+ final DateTime nextRetryDate = getNextRetryDate(retryAttempt);
+ if (nextRetryDate == null) {
+ return false;
+ }
+ return scheduleRetryFromTransaction(paymentId, nextRetryDate, transactionalDao);
+ }
+
+ private DateTime getNextRetryDate(final int retryAttempt) {
+
+ if (retryAttempt > config.getPluginFailureRetryMaxAttempts()) {
+ return null;
+ }
+ int nbSec = config.getPluginFailureRetryStart();
+ int remainingAttempts = retryAttempt;
+ while (--remainingAttempts > 0) {
+ nbSec = nbSec * config.getPluginFailureRetryMultiplier();
+ }
+ return clock.getUTCNow().plusSeconds(nbSec);
+ }
+ }
+
+ @Override
+ public String getQueueName() {
+ return QUEUE_NAME;
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/retry/RetryService.java b/payment/src/main/java/org/killbill/billing/payment/retry/RetryService.java
new file mode 100644
index 0000000..657848a
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/retry/RetryService.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.payment.retry;
+
+import java.util.UUID;
+
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists;
+import org.killbill.billing.callcontext.InternalCallContext;
+
+public interface RetryService {
+
+ public void initialize(final String svcName)
+ throws NotificationQueueAlreadyExists;
+
+ public void start();
+
+ public void stop()
+ throws NoSuchNotificationQueue;
+
+ public String getQueueName();
+
+ public void retry(UUID paymentId, final InternalCallContext context);
+
+}
diff --git a/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentAttemptSqlDao.sql.stg b/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentAttemptSqlDao.sql.stg
new file mode 100644
index 0000000..87ebd96
--- /dev/null
+++ b/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentAttemptSqlDao.sql.stg
@@ -0,0 +1,74 @@
+group PaymentAttemptSqlDao: EntitySqlDao;
+
+tableFields(prefix) ::= <<
+ <prefix>payment_id
+, <prefix>payment_method_id
+, <prefix>gateway_error_code
+, <prefix>gateway_error_msg
+, <prefix>processing_status
+, <prefix>requested_amount
+, <prefix>requested_currency
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>updated_by
+, <prefix>updated_date
+>>
+
+tableValues() ::= <<
+ :paymentId
+, :paymentMethodId
+, :gatewayErrorCode
+, :gatewayErrorMsg
+, :processingStatus
+, :requestedAmount
+, :requestedCurrency
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+tableName() ::= "payment_attempts"
+
+historyTableName() ::= "payment_attempt_history"
+
+
+getById(id) ::= <<
+select <allTableFields("pa.")>
+, pa.created_date as effective_date
+, p.account_id as account_id
+, p.invoice_id as invoice_id
+from <tableName()> pa join payments p
+where pa.id = :id
+and pa.payment_id = p.id
+<AND_CHECK_TENANT("pa.")>
+<AND_CHECK_TENANT("p.")>
+;
+>>
+
+getByPaymentId(paymentId) ::= <<
+select <allTableFields("pa.")>
+, pa.created_date as effective_date
+, p.account_id as account_id
+, p.invoice_id as invoice_id
+from <tableName()> pa join payments p
+where pa.payment_id = :paymentId
+and p.id = :paymentId
+<AND_CHECK_TENANT("pa.")>
+<AND_CHECK_TENANT("p.")>
+<defaultOrderBy("pa.")>
+;
+>>
+
+
+updatePaymentAttemptStatus() ::= <<
+update <tableName()>
+set processing_status = :processingStatus
+, gateway_error_code = :gatewayErrorCode
+, gateway_error_msg = :gatewayErrorMsg
+, updated_by = :updatedBy
+, updated_date = :createdDate
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
diff --git a/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentMethodSqlDao.sql.stg b/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentMethodSqlDao.sql.stg
new file mode 100644
index 0000000..a4135fb
--- /dev/null
+++ b/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentMethodSqlDao.sql.stg
@@ -0,0 +1,94 @@
+group PaymentMethodSqlDao: EntitySqlDao;
+
+
+
+tableName() ::= "payment_methods"
+
+historyTableName() ::= "payment_method_history"
+
+andCheckSoftDeletionWithComma(prefix) ::= "and <prefix>is_active"
+
+tableFields(prefix) ::= <<
+ <prefix>account_id
+, <prefix>plugin_name
+, <prefix>is_active
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>updated_by
+, <prefix>updated_date
+>>
+
+tableValues() ::= <<
+ :accountId
+, :pluginName
+, :isActive
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+
+markPaymentMethodAsDeleted(id) ::= <<
+update <tableName()>
+set is_active = 0
+, updated_by = :updatedBy
+, updated_date = :createdDate
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
+
+unmarkPaymentMethodAsDeleted(id) ::= <<
+update <tableName()>
+set is_active = 1
+, updated_by = :updatedBy
+, updated_date = :createdDate
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
+
+getPaymentMethodIncludedDelete(accountId) ::= <<
+select <allTableFields()>
+from <tableName()>
+where id = :id
+;
+>>
+
+getByAccountId(accountId) ::= <<
+select
+<allTableFields()>
+from <tableName()>
+where account_id = :accountId
+and is_active = 1
+;
+>>
+
+getByAccountIdIncludedDelete(accountId) ::= <<
+select
+<allTableFields()>
+from <tableName()>
+where account_id = :accountId
+;
+>>
+
+getByPluginName() ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+where t.plugin_name = :pluginName
+and t.is_active = 1
+order by t.record_id
+limit :offset, :rowCount
+;
+>>
+
+getCountByPluginName() ::= <<
+select
+ count(1) as count
+from <tableName()> t
+where t.plugin_name = :pluginName
+and t.is_active = 1
+;
+>>
diff --git a/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentSqlDao.sql.stg b/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentSqlDao.sql.stg
new file mode 100644
index 0000000..88bb4b0
--- /dev/null
+++ b/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentSqlDao.sql.stg
@@ -0,0 +1,125 @@
+group PaymentSqlDao: EntitySqlDao;
+
+
+extraTableFieldsWithComma(prefix) ::= <<
+, <prefix>record_id as payment_number
+>>
+
+defaultOrderBy(prefix) ::= <<
+order by <prefix>effective_date ASC, <recordIdField(prefix)> ASC
+>>
+
+tableFields(prefix) ::= <<
+ <prefix>account_id
+, <prefix>invoice_id
+, <prefix>payment_method_id
+, <prefix>amount
+, <prefix>currency
+, <prefix>processed_amount
+, <prefix>processed_currency
+, <prefix>effective_date
+, <prefix>payment_status
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>updated_by
+, <prefix>updated_date
+>>
+
+tableValues() ::= <<
+ :accountId
+, :invoiceId
+, :paymentMethodId
+, :amount
+, :currency
+, :processedAmount
+, :processedCurrency
+, :effectiveDate
+, :paymentStatus
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+tableName() ::= "payments"
+
+historyTableName() ::= "payment_history"
+
+
+getPaymentsForAccount() ::= <<
+select <allTableFields()>
+, record_id as payment_number
+from payments
+where account_id = :accountId
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+getPaymentsForInvoice() ::= <<
+select <allTableFields()>
+, record_id as payment_number
+from payments
+where invoice_id = :invoiceId
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+
+getLastPaymentForAccountAndPaymentMethod() ::= <<
+select <allTableFields()>
+, record_id as payment_number
+from payments
+where account_id = :accountId
+and payment_method_id = :paymentMethodId
+<AND_CHECK_TENANT()>
+order by effective_date desc limit 1
+;
+>>
+
+
+updatePaymentStatus() ::= <<
+update payments
+set payment_status = :paymentStatus
+, processed_amount = :processedAmount
+, processed_currency = :processedCurrency
+, updated_by = :updatedBy
+, updated_date = :createdDate
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
+
+
+updatePaymentForNewAttempt() ::= <<
+update <tableName()>
+set amount = :amount
+, effective_date = :effectiveDate
+, payment_method_id= :paymentMethodId
+, updated_by = :updatedBy
+, updated_date = :createdDate
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
+
+getByPluginName() ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+join payment_methods pm on pm.id = t.payment_method_id
+where pm.plugin_name = :pluginName
+order by record_id
+limit :offset, :rowCount
+;
+>>
+
+getCountByPluginName() ::= <<
+select
+ count(1) as count
+from <tableName()> t
+join payment_methods pm on pm.id = t.payment_method_id
+where pm.plugin_name = :pluginName
+;
+>>
diff --git a/payment/src/main/resources/org/killbill/billing/payment/dao/RefundSqlDao.sql.stg b/payment/src/main/resources/org/killbill/billing/payment/dao/RefundSqlDao.sql.stg
new file mode 100644
index 0000000..ff82e80
--- /dev/null
+++ b/payment/src/main/resources/org/killbill/billing/payment/dao/RefundSqlDao.sql.stg
@@ -0,0 +1,87 @@
+group RefundSqlDao: EntitySqlDao;
+
+tableName() ::= "refunds"
+
+historyTableName() ::= "refund_history"
+
+tableFields(prefix) ::= <<
+ <prefix>account_id
+, <prefix>payment_id
+, <prefix>amount
+, <prefix>currency
+, <prefix>processed_amount
+, <prefix>processed_currency
+, <prefix>is_adjusted
+, <prefix>refund_status
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>updated_by
+, <prefix>updated_date
+>>
+
+tableValues() ::= <<
+:accountId
+, :paymentId
+, :amount
+, :currency
+, :processedAmount
+, :processedCurrency
+, :isAdjusted
+, :refundStatus
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+updateStatus(refundStatus) ::= <<
+update <tableName()>
+set refund_status = :refundStatus
+, processed_amount = :processedAmount
+, processed_currency = :processedCurrency
+, updated_by = :updatedBy
+, updated_date = :createdDate
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
+
+getRefundsForPayment(paymentId) ::= <<
+select <allTableFields()>
+from <tableName()>
+where payment_id = :paymentId
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+getRefundsForAccount(accountId) ::= <<
+select <allTableFields()>
+from <tableName()>
+where account_id = :accountId
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+getByPluginName() ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+join payments p on p.id = t.payment_id
+join payment_methods pm on pm.id = p.payment_method_id
+where pm.plugin_name = :pluginName
+order by record_id
+limit :offset, :rowCount
+;
+>>
+
+getCountByPluginName() ::= <<
+select
+ count(1) as count
+from <tableName()> t
+join payments p on p.id = t.payment_id
+join payment_methods pm on pm.id = p.payment_method_id
+where pm.plugin_name = :pluginName
+;
+>>
diff --git a/payment/src/main/resources/org/killbill/billing/payment/ddl.sql b/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
new file mode 100644
index 0000000..cbeda72
--- /dev/null
+++ b/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
@@ -0,0 +1,196 @@
+/*! SET storage_engine=INNODB */;
+
+DROP TABLE IF EXISTS payments;
+CREATE TABLE payments (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ account_id char(36) NOT NULL,
+ invoice_id char(36) NOT NULL,
+ payment_method_id char(36) NOT NULL,
+ amount numeric(15,9),
+ currency char(3),
+ processed_amount numeric(15,9),
+ processed_currency char(3),
+ effective_date datetime,
+ payment_status varchar(50),
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY (record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX payments_id ON payments(id);
+CREATE INDEX payments_inv ON payments(invoice_id);
+CREATE INDEX payments_accnt ON payments(account_id);
+CREATE INDEX payments_tenant_account_record_id ON payments(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS payment_history;
+CREATE TABLE payment_history (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ target_record_id int(11) unsigned NOT NULL,
+ account_id char(36) NOT NULL,
+ invoice_id char(36) NOT NULL,
+ payment_method_id char(36) NOT NULL,
+ amount numeric(15,9),
+ currency char(3),
+ processed_amount numeric(15,9),
+ processed_currency char(3),
+ effective_date datetime,
+ payment_status varchar(50),
+ ext_first_payment_ref_id varchar(128),
+ ext_second_payment_ref_id varchar(128),
+ change_type char(6) NOT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX payment_history_target_record_id ON payment_history(target_record_id);
+CREATE INDEX payment_history_tenant_account_record_id ON payment_history(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS payment_attempts;
+CREATE TABLE payment_attempts (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ payment_id char(36) NOT NULL,
+ payment_method_id char(36) NOT NULL,
+ gateway_error_code varchar(32),
+ gateway_error_msg varchar(256),
+ processing_status varchar(50),
+ requested_amount numeric(15,9),
+ requested_currency char(3),
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY (record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX payment_attempts_id ON payment_attempts(id);
+CREATE INDEX payment_attempts_payment ON payment_attempts(payment_id);
+CREATE INDEX payment_attempts_tenant_account_record_id ON payment_attempts(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS payment_attempt_history;
+CREATE TABLE payment_attempt_history (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ target_record_id int(11) unsigned NOT NULL,
+ payment_id char(36) NOT NULL,
+ payment_method_id char(36) NOT NULL,
+ gateway_error_code varchar(32),
+ gateway_error_msg varchar(256),
+ processing_status varchar(50),
+ requested_amount numeric(15,9),
+ requested_currency char(3),
+ change_type char(6) NOT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX payment_attempt_history_target_record_id ON payment_attempt_history(target_record_id);
+CREATE INDEX payment_attempt_history_tenant_account_record_id ON payment_attempt_history(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS payment_methods;
+CREATE TABLE payment_methods (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ account_id char(36) NOT NULL,
+ plugin_name varchar(50) DEFAULT NULL,
+ is_active bool DEFAULT true,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY (record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX payment_methods_id ON payment_methods(id);
+CREATE INDEX payment_methods_active_accnt ON payment_methods(is_active, account_id);
+CREATE INDEX payment_methods_tenant_account_record_id ON payment_methods(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS payment_method_history;
+CREATE TABLE payment_method_history (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ target_record_id int(11) unsigned NOT NULL,
+ account_id char(36) NOT NULL,
+ plugin_name varchar(50) DEFAULT NULL,
+ is_active bool DEFAULT true,
+ change_type char(6) NOT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX payment_method_history_target_record_id ON payment_method_history(target_record_id);
+CREATE INDEX payment_method_history_tenant_account_record_id ON payment_method_history(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS refunds;
+CREATE TABLE refunds (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ account_id char(36) NOT NULL,
+ payment_id char(36) NOT NULL,
+ amount numeric(15,9),
+ currency char(3),
+ processed_amount numeric(15,9),
+ processed_currency char(3),
+ is_adjusted tinyint(1),
+ refund_status varchar(50),
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY (record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX refunds_id ON refunds(id);
+CREATE INDEX refunds_pay ON refunds(payment_id);
+CREATE INDEX refunds_accnt ON refunds(account_id);
+CREATE INDEX refunds_tenant_account_record_id ON refunds(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS refund_history;
+CREATE TABLE refund_history (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ target_record_id int(11) unsigned NOT NULL,
+ account_id char(36) NOT NULL,
+ payment_id char(36) NOT NULL,
+ amount numeric(15,9),
+ currency char(3),
+ processed_amount numeric(15,9),
+ processed_currency char(3),
+ is_adjusted tinyint(1),
+ refund_status varchar(50),
+ change_type char(6) NOT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX refund_history_target_record_id ON refund_history(target_record_id);
+CREATE INDEX refund_history_tenant_account_record_id ON refund_history(tenant_record_id, account_record_id);
+
+
+
+
+
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestEventJson.java b/payment/src/test/java/org/killbill/billing/payment/api/TestEventJson.java
new file mode 100644
index 0000000..64703fe
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestEventJson.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.events.PaymentErrorInternalEvent;
+import org.killbill.billing.events.PaymentInfoInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+public class TestEventJson extends PaymentTestSuiteNoDB {
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Test(groups = "fast")
+ public void testPaymentErrorEvent() throws Exception {
+ final PaymentErrorInternalEvent e = new DefaultPaymentErrorEvent(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), "no message", 1L, 2L, UUID.randomUUID());
+ final String json = mapper.writeValueAsString(e);
+
+ final Class<?> claz = Class.forName(DefaultPaymentErrorEvent.class.getName());
+ final Object obj = mapper.readValue(json, claz);
+ Assert.assertTrue(obj.equals(e));
+ }
+
+ @Test(groups = "fast")
+ public void testPaymentInfoEvent() throws Exception {
+ final PaymentInfoInternalEvent e = new DefaultPaymentInfoEvent(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), new BigDecimal(12.9), new Integer(13), PaymentStatus.SUCCESS,
+ new DateTime(), 1L, 2L, UUID.randomUUID());
+ final String json = mapper.writeValueAsString(e);
+
+ final Class<?> clazz = Class.forName(DefaultPaymentInfoEvent.class.getName());
+ final Object obj = mapper.readValue(json, clazz);
+ Assert.assertTrue(obj.equals(e));
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
new file mode 100644
index 0000000..1b38d97
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.bus.api.PersistentBus.EventBusException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.payment.MockRecurringInvoiceItem;
+import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
+
+public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
+
+
+ private Account account;
+
+ @BeforeClass(groups = "slow")
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ account = testHelper.createTestAccount("bobo@gmail.com", false);
+ }
+
+
+ @Test(groups = "slow")
+ public void testCreatePaymentWithNoDefaultPaymentMethod() throws InvoiceApiException, EventBusException, PaymentApiException {
+
+
+ final LocalDate now = clock.getUTCToday();
+ final Invoice invoice = testHelper.createTestInvoice(account, now, Currency.USD, callContext);
+
+ final UUID subscriptionId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final BigDecimal requestedAmount = BigDecimal.TEN;
+
+ invoice.addInvoiceItem(new MockRecurringInvoiceItem(invoice.getId(), account.getId(),
+ subscriptionId,
+ bundleId,
+ "test plan", "test phase",
+ now,
+ now.plusMonths(1),
+ requestedAmount,
+ new BigDecimal("1.0"),
+ Currency.USD));
+
+ try {
+ paymentApi.createPayment(account, invoice.getId(), requestedAmount, callContext);
+ } catch (PaymentApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD.getCode());
+ }
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), callContext);
+ Assert.assertEquals(payments.size(), 1);
+
+ final Payment payment = payments.get(0);
+ Assert.assertEquals(payment.getPaymentStatus(), PaymentStatus.PAYMENT_FAILURE_ABORTED);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
new file mode 100644
index 0000000..e45c453
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiNoDB.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.payment.MockRecurringInvoiceItem;
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.payment.provider.DefaultNoOpPaymentMethodPlugin;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class TestPaymentApiNoDB extends PaymentTestSuiteNoDB {
+
+ private static final Logger log = LoggerFactory.getLogger(TestPaymentApiNoDB.class);
+
+ private Account account;
+
+ @BeforeClass(groups = "fast")
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ account = testHelper.createTestAccount("yoyo.yahoo.com", false);
+ }
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ final PaymentMethodPlugin paymentMethodInfo = new DefaultNoOpPaymentMethodPlugin(UUID.randomUUID().toString(), true, null);
+ testHelper.addTestPaymentMethod(account, paymentMethodInfo);
+ }
+
+ @Test(groups = "fast")
+ public void testSimplePaymentWithNoAmount() throws Exception {
+ final BigDecimal invoiceAmount = new BigDecimal("10.0011");
+ final BigDecimal requestedAmount = null;
+ final BigDecimal expectedAmount = invoiceAmount;
+
+ testSimplePayment(invoiceAmount, requestedAmount, expectedAmount);
+ }
+
+ @Test(groups = "fast")
+ public void testSimplePaymentWithInvoiceAmount() throws Exception {
+ final BigDecimal invoiceAmount = new BigDecimal("10.0011");
+ final BigDecimal requestedAmount = invoiceAmount;
+ final BigDecimal expectedAmount = invoiceAmount;
+
+ testSimplePayment(invoiceAmount, requestedAmount, expectedAmount);
+ }
+
+ @Test(groups = "fast")
+ public void testSimplePaymentWithLowerAmount() throws Exception {
+ final BigDecimal invoiceAmount = new BigDecimal("10.0011");
+ final BigDecimal requestedAmount = new BigDecimal("8.0091");
+ final BigDecimal expectedAmount = requestedAmount;
+
+ testSimplePayment(invoiceAmount, requestedAmount, expectedAmount);
+ }
+
+ @Test(groups = "fast")
+ public void testSimplePaymentWithInvalidAmount() throws Exception {
+ final BigDecimal invoiceAmount = new BigDecimal("10.0011");
+ final BigDecimal requestedAmount = new BigDecimal("80.0091");
+ final BigDecimal expectedAmount = null;
+
+ testSimplePayment(invoiceAmount, requestedAmount, expectedAmount);
+ }
+
+ private void testSimplePayment(final BigDecimal invoiceAmount, final BigDecimal requestedAmount, final BigDecimal expectedAmount) throws Exception {
+ final LocalDate now = clock.getUTCToday();
+ final Invoice invoice = testHelper.createTestInvoice(account, now, Currency.USD, callContext);
+
+ final UUID subscriptionId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+
+ invoice.addInvoiceItem(new MockRecurringInvoiceItem(invoice.getId(), account.getId(),
+ subscriptionId,
+ bundleId,
+ "test plan", "test phase",
+ now,
+ now.plusMonths(1),
+ invoiceAmount,
+ new BigDecimal("1.0"),
+ Currency.USD));
+
+ try {
+ final Payment paymentInfo = paymentApi.createPayment(account, invoice.getId(), requestedAmount, callContext);
+ if (expectedAmount == null) {
+ fail("Expected to fail because requested amount > invoice amount");
+ }
+ assertNotNull(paymentInfo.getId());
+ assertTrue(paymentInfo.getAmount().compareTo(expectedAmount) == 0);
+ assertNotNull(paymentInfo.getPaymentNumber());
+ assertEquals(paymentInfo.getPaymentStatus(), PaymentStatus.SUCCESS);
+ assertEquals(paymentInfo.getAttempts().size(), 1);
+ assertEquals(paymentInfo.getInvoiceId(), invoice.getId());
+ assertEquals(paymentInfo.getCurrency(), Currency.USD);
+
+ final PaymentAttempt paymentAttempt = paymentInfo.getAttempts().get(0);
+ assertNotNull(paymentAttempt);
+ assertNotNull(paymentAttempt.getId());
+ } catch (PaymentApiException e) {
+ if (expectedAmount != null) {
+ fail("Failed to create payment", e);
+ } else {
+ log.info(e.getMessage());
+ assertEquals(e.getCode(), ErrorCode.PAYMENT_AMOUNT_DENIED.getCode());
+ }
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testPaymentMethods() throws Exception {
+ List<PaymentMethod> methods = paymentApi.getPaymentMethods(account, false, callContext);
+ assertEquals(methods.size(), 1);
+
+ final PaymentMethod initDefaultMethod = methods.get(0);
+ assertEquals(initDefaultMethod.getId(), account.getPaymentMethodId());
+
+ final PaymentMethodPlugin newPaymenrMethod = new DefaultNoOpPaymentMethodPlugin(UUID.randomUUID().toString(), true, null);
+ final UUID newPaymentMethodId = paymentApi.addPaymentMethod(MockPaymentProviderPlugin.PLUGIN_NAME, account, true, newPaymenrMethod, callContext);
+ Mockito.when(account.getPaymentMethodId()).thenReturn(newPaymentMethodId);
+
+ methods = paymentApi.getPaymentMethods(account, false, callContext);
+ assertEquals(methods.size(), 2);
+
+ assertEquals(newPaymentMethodId, account.getPaymentMethodId());
+
+ boolean failed = false;
+ try {
+ paymentApi.deletedPaymentMethod(account, newPaymentMethodId, false, callContext);
+ } catch (PaymentApiException e) {
+ failed = true;
+ }
+ assertTrue(failed);
+
+ paymentApi.deletedPaymentMethod(account, initDefaultMethod.getId(), true, callContext);
+ methods = paymentApi.getPaymentMethods(account, false, callContext);
+ assertEquals(methods.size(), 1);
+
+ // NOW retry with default payment method with special flag
+ paymentApi.deletedPaymentMethod(account, newPaymentMethodId, true, callContext);
+
+ methods = paymentApi.getPaymentMethods(account, false, callContext);
+ assertEquals(methods.size(), 0);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentMethodPlugin.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentMethodPlugin.java
new file mode 100644
index 0000000..b393341
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentMethodPlugin.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.util.List;
+import java.util.UUID;
+
+public class TestPaymentMethodPlugin extends TestPaymentMethodPluginBase implements PaymentMethodPlugin {
+
+ private final UUID kbPaymentMethodId;
+ private final String externalPaymentMethodId;
+ private final boolean isDefaultPaymentMethod;
+ private final List<PaymentMethodKVInfo> properties;
+
+ public TestPaymentMethodPlugin(final UUID kbPaymentMethodId, final PaymentMethodPlugin src, final String externalPaymentId) {
+ this.kbPaymentMethodId = kbPaymentMethodId;
+ this.externalPaymentMethodId = externalPaymentId;
+ this.isDefaultPaymentMethod = src.isDefaultPaymentMethod();
+ this.properties = src.getProperties();
+ }
+
+ @Override
+ public UUID getKbPaymentMethodId() {
+ return kbPaymentMethodId;
+ }
+
+ @Override
+ public String getExternalPaymentMethodId() {
+ return externalPaymentMethodId;
+ }
+
+ @Override
+ public boolean isDefaultPaymentMethod() {
+ return isDefaultPaymentMethod;
+ }
+
+ @Override
+ public List<PaymentMethodKVInfo> getProperties() {
+ return properties;
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java
new file mode 100644
index 0000000..1277751
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorNoDB.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.core;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.payment.api.PaymentMethod;
+import org.killbill.billing.payment.provider.ExternalPaymentProviderPlugin;
+
+public class TestPaymentMethodProcessorNoDB extends PaymentTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testGetExternalPaymentProviderPlugin() throws Exception {
+ final UUID accountId = UUID.randomUUID();
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getId()).thenReturn(accountId);
+ Mockito.when(account.getExternalKey()).thenReturn(accountId.toString());
+
+ Assert.assertEquals(paymentMethodProcessor.getPaymentMethods(account, false, internalCallContext).size(), 0);
+
+ // The first call should create the payment method
+ final ExternalPaymentProviderPlugin providerPlugin = paymentMethodProcessor.getExternalPaymentProviderPlugin(account, internalCallContext);
+ final List<PaymentMethod> paymentMethods = paymentMethodProcessor.getPaymentMethods(account, false, internalCallContext);
+ Assert.assertEquals(paymentMethods.size(), 1);
+ Assert.assertEquals(paymentMethods.get(0).getPluginName(), ExternalPaymentProviderPlugin.PLUGIN_NAME);
+ Assert.assertEquals(paymentMethods.get(0).getAccountId(), account.getId());
+
+ // The succeeding calls should not create any other payment method
+ final UUID externalPaymentMethodId = paymentMethods.get(0).getId();
+ for (int i = 0; i < 50; i++) {
+ final ExternalPaymentProviderPlugin foundProviderPlugin = paymentMethodProcessor.getExternalPaymentProviderPlugin(account, internalCallContext);
+ Assert.assertNotNull (foundProviderPlugin);
+
+ final List<PaymentMethod> foundPaymentMethods = paymentMethodProcessor.getPaymentMethods(account, false, internalCallContext);
+ Assert.assertEquals(foundPaymentMethods.size(), 1);
+ Assert.assertEquals(foundPaymentMethods.get(0).getPluginName(), ExternalPaymentProviderPlugin.PLUGIN_NAME);
+ Assert.assertEquals(foundPaymentMethods.get(0).getAccountId(), account.getId());
+ Assert.assertEquals(foundPaymentMethods.get(0).getId(), externalPaymentMethodId);
+ }
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorRefreshWithDB.java b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorRefreshWithDB.java
new file mode 100644
index 0000000..b4b4bd9
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorRefreshWithDB.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.core;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
+import org.killbill.billing.payment.api.PaymentMethod;
+import org.killbill.billing.payment.api.PaymentMethodKVInfo;
+import org.killbill.billing.payment.dao.PaymentMethodModelDao;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.provider.DefaultNoOpPaymentMethodPlugin;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestPaymentMethodProcessorRefreshWithDB extends PaymentTestSuiteWithEmbeddedDB {
+
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ getPluginApi().resetPaymentMethods(null, null);
+ }
+
+ @Test(groups = "slow")
+ public void testRefreshWithNewPaymentMethod() throws Exception {
+
+ final Account account = testHelper.createTestAccount("foo@bar.com", true);
+ Assert.assertEquals(getPluginApi().getPaymentMethods(account.getId(), true, callContext).size(), 1);
+ final UUID existingPMId = account.getPaymentMethodId();
+
+ // Add new payment in plugin directly
+ final UUID newPmId = UUID.randomUUID();
+ getPluginApi().addPaymentMethod(account.getId(), newPmId, new DefaultNoOpPaymentMethodPlugin(UUID.randomUUID().toString(), false, ImmutableList.<PaymentMethodKVInfo>of()), false, callContext);
+
+ // Verify that the refresh does indeed show 2 PMs
+ final List<PaymentMethod> methods = paymentMethodProcessor.refreshPaymentMethods(MockPaymentProviderPlugin.PLUGIN_NAME, account, internalCallContext);
+ Assert.assertEquals(methods.size(), 2);
+ checkPaymentMethodExistsWithStatus(methods, existingPMId, true);
+ checkPaymentMethodExistsWithStatus(methods, newPmId, true);
+ }
+
+
+ @Test(groups = "slow")
+ public void testRefreshWithDeletedPaymentMethod() throws Exception {
+
+ final Account account = testHelper.createTestAccount("super@bar.com", true);
+ Assert.assertEquals(getPluginApi().getPaymentMethods(account.getId(), true, callContext).size(), 1);
+ final UUID firstPmId = account.getPaymentMethodId();
+
+ final UUID secondPmId = paymentApi.addPaymentMethod(MockPaymentProviderPlugin.PLUGIN_NAME, account, true, new DefaultNoOpPaymentMethodPlugin(UUID.randomUUID().toString(), false, null), callContext);
+ Assert.assertEquals(getPluginApi().getPaymentMethods(account.getId(), true, callContext).size(), 2);
+ Assert.assertEquals(paymentApi.getPaymentMethods(account, false, callContext).size(), 2);
+
+ // Remove second PM from plugin
+ getPluginApi().deletePaymentMethod(account.getId(), secondPmId, callContext);
+ Assert.assertEquals(getPluginApi().getPaymentMethods(account.getId(), true, callContext).size(), 1);
+ Assert.assertEquals(paymentApi.getPaymentMethods(account, false, callContext).size(), 2);
+
+ // Verify that the refresh sees that PM as being deleted now
+ final List<PaymentMethod> methods = paymentMethodProcessor.refreshPaymentMethods(MockPaymentProviderPlugin.PLUGIN_NAME, account, internalCallContext);
+ Assert.assertEquals(methods.size(), 1);
+ checkPaymentMethodExistsWithStatus(methods, firstPmId, true);
+
+ PaymentMethodModelDao deletedPMModel = paymentDao.getPaymentMethodIncludedDeleted(secondPmId, internalCallContext);
+ Assert.assertNotNull(deletedPMModel);
+ Assert.assertFalse(deletedPMModel.isActive());
+ }
+
+
+ private void checkPaymentMethodExistsWithStatus(final List<PaymentMethod> methods, UUID expectedPaymentMethodId, boolean expectedActive) {
+ PaymentMethod foundPM = null;
+ for (PaymentMethod cur : methods) {
+ if (cur.getId().equals(expectedPaymentMethodId)) {
+ foundPM = cur;
+ break;
+ }
+ }
+ Assert.assertNotNull(foundPM);
+ Assert.assertEquals(foundPM.isActive().booleanValue(), expectedActive);
+ }
+
+
+ private PaymentPluginApi getPluginApi() {
+ final PaymentPluginApi pluginApi = registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME);
+ Assert.assertNotNull(pluginApi);
+ return pluginApi;
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
new file mode 100644
index 0000000..892bc51
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.api.PaymentStatus;
+import org.killbill.billing.payment.api.RefundStatus;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.collect.ImmutableList;
+
+public class MockPaymentDao implements PaymentDao {
+
+ private final Map<UUID, PaymentModelDao> payments = new HashMap<UUID, PaymentModelDao>();
+ private final Map<UUID, PaymentAttemptModelDao> attempts = new HashMap<UUID, PaymentAttemptModelDao>();
+
+ @Override
+ public PaymentModelDao insertPaymentWithFirstAttempt(final PaymentModelDao paymentInfo, final PaymentAttemptModelDao attempt,
+ final InternalCallContext context) {
+ synchronized (this) {
+ payments.put(paymentInfo.getId(), paymentInfo);
+ attempts.put(attempt.getId(), attempt);
+ }
+ return paymentInfo;
+ }
+
+ @Override
+ public PaymentAttemptModelDao updatePaymentWithNewAttempt(final UUID paymentId, final PaymentAttemptModelDao attempt, final InternalCallContext context) {
+ synchronized (this) {
+ attempts.put(attempt.getId(), attempt);
+ }
+ return attempt;
+ }
+
+ @Override
+ public void updatePaymentAndAttemptOnCompletion(final UUID paymentId, final PaymentStatus paymentStatus,
+ BigDecimal processedAmount, Currency processedCurrency,
+ final UUID attemptId, final String gatewayErrorCode,
+ final String gatewayErrorMsg,
+ final InternalCallContext context) {
+ synchronized (this) {
+ final PaymentModelDao entry = payments.remove(paymentId);
+ if (entry != null) {
+ payments.put(paymentId, new PaymentModelDao(entry, paymentStatus));
+ }
+ final PaymentAttemptModelDao tmp = attempts.remove(attemptId);
+ if (tmp != null) {
+ attempts.put(attemptId, new PaymentAttemptModelDao(tmp, paymentStatus, gatewayErrorCode, gatewayErrorMsg));
+ }
+ }
+ }
+
+ @Override
+ public PaymentAttemptModelDao getPaymentAttempt(final UUID attemptId, final InternalTenantContext context) {
+ return attempts.get(attemptId);
+ }
+
+ @Override
+ public List<PaymentModelDao> getPaymentsForInvoice(final UUID invoiceId, final InternalTenantContext context) {
+ final List<PaymentModelDao> result = new ArrayList<PaymentModelDao>();
+ synchronized (this) {
+ for (final PaymentModelDao cur : payments.values()) {
+ if (cur.getInvoiceId().equals(invoiceId)) {
+ result.add(cur);
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public List<PaymentModelDao> getPaymentsForAccount(final UUID accountId, final InternalTenantContext context) {
+ final List<PaymentModelDao> result = new ArrayList<PaymentModelDao>();
+ synchronized (this) {
+ for (final PaymentModelDao cur : payments.values()) {
+ if (cur.getAccountId().equals(accountId)) {
+ result.add(cur);
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Pagination<PaymentModelDao> getPayments(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PaymentModelDao getPayment(final UUID paymentId, final InternalTenantContext context) {
+ return payments.get(paymentId);
+ }
+
+ @Override
+ public List<PaymentAttemptModelDao> getAttemptsForPayment(final UUID paymentId, final InternalTenantContext context) {
+ final List<PaymentAttemptModelDao> result = new ArrayList<PaymentAttemptModelDao>();
+ synchronized (this) {
+ for (final PaymentAttemptModelDao cur : attempts.values()) {
+ if (cur.getPaymentId().equals(paymentId)) {
+ result.add(cur);
+ }
+ }
+ }
+ return result;
+ }
+
+ private final List<PaymentMethodModelDao> paymentMethods = new LinkedList<PaymentMethodModelDao>();
+
+ @Override
+ public PaymentMethodModelDao insertPaymentMethod(final PaymentMethodModelDao paymentMethod, final InternalCallContext context) {
+ paymentMethods.add(paymentMethod);
+ return paymentMethod;
+ }
+
+ @Override
+ public PaymentMethodModelDao getPaymentMethod(final UUID paymentMethodId, final InternalTenantContext context) {
+ for (final PaymentMethodModelDao cur : paymentMethods) {
+ if (cur.getId().equals(paymentMethodId)) {
+ return cur;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public List<PaymentMethodModelDao> getPaymentMethods(final UUID accountId, final InternalTenantContext context) {
+ final List<PaymentMethodModelDao> result = new ArrayList<PaymentMethodModelDao>();
+ for (final PaymentMethodModelDao cur : paymentMethods) {
+ if (cur.getAccountId().equals(accountId)) {
+ result.add(cur);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Pagination<PaymentMethodModelDao> getPaymentMethods(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void deletedPaymentMethod(final UUID paymentMethodId, final InternalCallContext context) {
+ final Iterator<PaymentMethodModelDao> it = paymentMethods.iterator();
+ while (it.hasNext()) {
+ final PaymentMethodModelDao cur = it.next();
+ if (cur.getId().equals(paymentMethodId)) {
+ it.remove();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public List<PaymentMethodModelDao> refreshPaymentMethods(final UUID accountId, final String pluginName, final List<PaymentMethodModelDao> paymentMethods, final InternalCallContext context) {
+ return ImmutableList.<PaymentMethodModelDao>of();
+ }
+
+ @Override
+ public void undeletedPaymentMethod(final UUID paymentMethodId, final InternalCallContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public RefundModelDao insertRefund(final RefundModelDao refundInfo, final InternalCallContext context) {
+ return null;
+ }
+
+ @Override
+ public void updateRefundStatus(final UUID refundId, final RefundStatus status, final BigDecimal processedAmount, final Currency processedCurrency, final InternalCallContext context) {
+ return;
+ }
+
+ @Override
+ public Pagination<RefundModelDao> getRefunds(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public RefundModelDao getRefund(final UUID refundId, final InternalTenantContext context) {
+ return null;
+ }
+
+ @Override
+ public List<RefundModelDao> getRefundsForPayment(final UUID paymentId, final InternalTenantContext context) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<RefundModelDao> getRefundsForAccount(final UUID accountId, final InternalTenantContext context) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public PaymentModelDao getLastPaymentForPaymentMethod(final UUID accountId, final UUID paymentMethodId, final InternalTenantContext context) {
+ return null;
+ }
+
+ @Override
+ public PaymentMethodModelDao getPaymentMethodIncludedDeleted(final UUID paymentMethodId, final InternalTenantContext context) {
+ return getPaymentMethod(paymentMethodId, context);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
new file mode 100644
index 0000000..d12107b
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
+import org.killbill.billing.payment.api.PaymentStatus;
+import org.killbill.billing.payment.api.RefundStatus;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.fail;
+
+public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testRefund() {
+ final UUID accountId = UUID.randomUUID();
+ final UUID paymentId1 = UUID.randomUUID();
+ final BigDecimal amount1 = new BigDecimal(13);
+ final Currency currency = Currency.USD;
+
+ final RefundModelDao refund1 = new RefundModelDao(accountId, paymentId1, amount1, currency, amount1, currency, true);
+
+ paymentDao.insertRefund(refund1, internalCallContext);
+ final RefundModelDao refundCheck = paymentDao.getRefund(refund1.getId(), internalCallContext);
+ assertNotNull(refundCheck);
+ assertEquals(refundCheck.getAccountId(), accountId);
+ assertEquals(refundCheck.getPaymentId(), paymentId1);
+ assertEquals(refundCheck.getAmount().compareTo(amount1), 0);
+ assertEquals(refundCheck.getCurrency(), currency);
+ assertEquals(refundCheck.isAdjusted(), true);
+ assertEquals(refundCheck.getRefundStatus(), RefundStatus.CREATED);
+
+ final BigDecimal amount2 = new BigDecimal(7.00);
+ final UUID paymentId2 = UUID.randomUUID();
+
+ RefundModelDao refund2 = new RefundModelDao(accountId, paymentId2, amount2, currency, amount2, currency, true);
+ paymentDao.insertRefund(refund2, internalCallContext);
+ paymentDao.updateRefundStatus(refund2.getId(), RefundStatus.COMPLETED, amount2, currency, internalCallContext);
+
+ List<RefundModelDao> refundChecks = paymentDao.getRefundsForPayment(paymentId1, internalCallContext);
+ assertEquals(refundChecks.size(), 1);
+
+ refundChecks = paymentDao.getRefundsForPayment(paymentId2, internalCallContext);
+ assertEquals(refundChecks.size(), 1);
+
+ refundChecks = paymentDao.getRefundsForAccount(accountId, internalCallContext);
+ assertEquals(refundChecks.size(), 2);
+ for (RefundModelDao cur : refundChecks) {
+ if (cur.getPaymentId().equals(paymentId1)) {
+ assertEquals(cur.getAmount().compareTo(amount1), 0);
+ assertEquals(cur.getRefundStatus(), RefundStatus.CREATED);
+ } else if (cur.getPaymentId().equals(paymentId2)) {
+ assertEquals(cur.getAmount().compareTo(amount2), 0);
+ assertEquals(cur.getRefundStatus(), RefundStatus.COMPLETED);
+ } else {
+ fail("Unexpected refund");
+ }
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testUpdateStatus() {
+ final UUID accountId = UUID.randomUUID();
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID paymentMethodId = UUID.randomUUID();
+ final BigDecimal amount = new BigDecimal(13);
+ final Currency currency = Currency.USD;
+ final DateTime effectiveDate = clock.getUTCNow();
+
+ final PaymentModelDao payment = new PaymentModelDao(accountId, invoiceId, paymentMethodId, amount, currency, effectiveDate);
+ final PaymentAttemptModelDao attempt = new PaymentAttemptModelDao(accountId, invoiceId, payment.getId(), paymentMethodId, effectiveDate, amount, currency);
+ PaymentModelDao savedPayment = paymentDao.insertPaymentWithFirstAttempt(payment, attempt, internalCallContext);
+ assertEquals(savedPayment.getEffectiveDate().compareTo(effectiveDate), 0);
+
+ final PaymentStatus paymentStatus = PaymentStatus.SUCCESS;
+ final String gatewayErrorCode = "OK";
+
+ clock.addDays(1);
+ paymentDao.updatePaymentAndAttemptOnCompletion(payment.getId(), paymentStatus, amount, currency, attempt.getId(), gatewayErrorCode, null, internalCallContext);
+
+ final List<PaymentModelDao> payments = paymentDao.getPaymentsForInvoice(invoiceId, internalCallContext);
+ assertEquals(payments.size(), 1);
+ savedPayment = payments.get(0);
+ assertEquals(savedPayment.getId(), payment.getId());
+ assertEquals(savedPayment.getAccountId(), accountId);
+ assertEquals(savedPayment.getInvoiceId(), invoiceId);
+ assertEquals(savedPayment.getPaymentMethodId(), paymentMethodId);
+ assertEquals(savedPayment.getAmount().compareTo(amount), 0);
+ assertEquals(savedPayment.getCurrency(), currency);
+ assertEquals(savedPayment.getEffectiveDate().compareTo(effectiveDate), 0);
+ assertEquals(savedPayment.getPaymentStatus(), PaymentStatus.SUCCESS);
+
+ final List<PaymentAttemptModelDao> attempts = paymentDao.getAttemptsForPayment(payment.getId(), internalCallContext);
+ assertEquals(attempts.size(), 1);
+ final PaymentAttemptModelDao savedAttempt = attempts.get(0);
+ assertEquals(savedAttempt.getId(), attempt.getId());
+ assertEquals(savedAttempt.getPaymentId(), payment.getId());
+ assertEquals(savedAttempt.getAccountId(), accountId);
+ assertEquals(savedAttempt.getInvoiceId(), invoiceId);
+ assertEquals(savedAttempt.getProcessingStatus(), PaymentStatus.SUCCESS);
+ assertEquals(savedAttempt.getGatewayErrorCode(), gatewayErrorCode);
+ assertEquals(savedAttempt.getRequestedAmount().compareTo(amount), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testPaymentWithAttempt() {
+ final UUID accountId = UUID.randomUUID();
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID paymentMethodId = UUID.randomUUID();
+ final BigDecimal amount = new BigDecimal(13);
+ final Currency currency = Currency.USD;
+ final DateTime effectiveDate = clock.getUTCNow();
+
+ final PaymentModelDao payment = new PaymentModelDao(accountId, invoiceId, paymentMethodId, amount, currency, effectiveDate);
+ final PaymentAttemptModelDao attempt = new PaymentAttemptModelDao(accountId, invoiceId, payment.getId(), paymentMethodId, clock.getUTCNow(), amount, currency);
+
+ PaymentModelDao savedPayment = paymentDao.insertPaymentWithFirstAttempt(payment, attempt, internalCallContext);
+ assertEquals(savedPayment.getId(), payment.getId());
+ assertEquals(savedPayment.getAccountId(), accountId);
+ assertEquals(savedPayment.getInvoiceId(), invoiceId);
+ assertEquals(savedPayment.getPaymentMethodId(), paymentMethodId);
+ assertEquals(savedPayment.getAmount().compareTo(amount), 0);
+ assertEquals(savedPayment.getCurrency(), currency);
+ assertEquals(savedPayment.getEffectiveDate().compareTo(effectiveDate), 0);
+ assertEquals(savedPayment.getPaymentStatus(), PaymentStatus.UNKNOWN);
+
+ PaymentAttemptModelDao savedAttempt = paymentDao.getPaymentAttempt(attempt.getId(), internalCallContext);
+ assertEquals(savedAttempt.getId(), attempt.getId());
+ assertEquals(savedAttempt.getPaymentId(), payment.getId());
+ assertEquals(savedAttempt.getAccountId(), accountId);
+ assertEquals(savedAttempt.getInvoiceId(), invoiceId);
+ assertEquals(savedAttempt.getProcessingStatus(), PaymentStatus.UNKNOWN);
+
+ final List<PaymentModelDao> payments = paymentDao.getPaymentsForInvoice(invoiceId, internalCallContext);
+ assertEquals(payments.size(), 1);
+ savedPayment = payments.get(0);
+ assertEquals(savedPayment.getId(), payment.getId());
+ assertEquals(savedPayment.getAccountId(), accountId);
+ assertEquals(savedPayment.getInvoiceId(), invoiceId);
+ assertEquals(savedPayment.getPaymentMethodId(), paymentMethodId);
+ assertEquals(savedPayment.getAmount().compareTo(amount), 0);
+ assertEquals(savedPayment.getCurrency(), currency);
+ assertEquals(savedPayment.getEffectiveDate().compareTo(effectiveDate), 0);
+ assertEquals(savedPayment.getPaymentStatus(), PaymentStatus.UNKNOWN);
+
+ final List<PaymentAttemptModelDao> attempts = paymentDao.getAttemptsForPayment(payment.getId(), internalCallContext);
+ assertEquals(attempts.size(), 1);
+ savedAttempt = attempts.get(0);
+ assertEquals(savedAttempt.getId(), attempt.getId());
+ assertEquals(savedAttempt.getPaymentId(), payment.getId());
+ assertEquals(savedAttempt.getAccountId(), accountId);
+ assertEquals(savedAttempt.getInvoiceId(), invoiceId);
+ assertEquals(savedAttempt.getProcessingStatus(), PaymentStatus.UNKNOWN);
+
+ }
+
+ @Test(groups = "slow")
+ public void testNewAttempt() {
+ final UUID accountId = UUID.randomUUID();
+ final UUID invoiceId = UUID.randomUUID();
+ final UUID paymentMethodId = UUID.randomUUID();
+ final BigDecimal amount = new BigDecimal(13);
+ final Currency currency = Currency.USD;
+ final DateTime effectiveDate = clock.getUTCNow();
+
+ final PaymentModelDao payment = new PaymentModelDao(accountId, invoiceId, paymentMethodId, amount, currency, effectiveDate);
+ final PaymentAttemptModelDao firstAttempt = new PaymentAttemptModelDao(accountId, invoiceId, payment.getId(), paymentMethodId, effectiveDate, amount, currency);
+ PaymentModelDao savedPayment = paymentDao.insertPaymentWithFirstAttempt(payment, firstAttempt, internalCallContext);
+
+ final PaymentModelDao lastPayment = paymentDao.getLastPaymentForPaymentMethod(accountId, paymentMethodId, internalCallContext);
+ assertNotNull(lastPayment);
+ assertEquals(lastPayment.getId(), payment.getId());
+ assertEquals(lastPayment.getAccountId(), accountId);
+ assertEquals(lastPayment.getInvoiceId(), invoiceId);
+ assertEquals(lastPayment.getPaymentMethodId(), paymentMethodId);
+ assertEquals(lastPayment.getAmount().compareTo(amount), 0);
+ assertEquals(lastPayment.getCurrency(), currency);
+ assertEquals(lastPayment.getEffectiveDate().compareTo(effectiveDate), 0);
+ assertEquals(lastPayment.getPaymentStatus(), PaymentStatus.UNKNOWN);
+
+ clock.addDays(3);
+ final DateTime newEffectiveDate = clock.getUTCNow();
+ final UUID newPaymentMethodId = UUID.randomUUID();
+ final BigDecimal newAmount = new BigDecimal("15.23");
+ final PaymentAttemptModelDao secondAttempt = new PaymentAttemptModelDao(accountId, invoiceId, payment.getId(), newPaymentMethodId, newEffectiveDate, newAmount, currency);
+ paymentDao.updatePaymentWithNewAttempt(payment.getId(), secondAttempt, internalCallContext);
+
+ final List<PaymentModelDao> payments = paymentDao.getPaymentsForInvoice(invoiceId, internalCallContext);
+ assertEquals(payments.size(), 1);
+ savedPayment = payments.get(0);
+ assertEquals(savedPayment.getId(), payment.getId());
+ assertEquals(savedPayment.getAccountId(), accountId);
+ assertEquals(savedPayment.getInvoiceId(), invoiceId);
+ assertEquals(savedPayment.getPaymentMethodId(), newPaymentMethodId);
+ assertEquals(savedPayment.getAmount().compareTo(newAmount), 0);
+ assertEquals(savedPayment.getCurrency(), currency);
+ assertEquals(savedPayment.getEffectiveDate().compareTo(newEffectiveDate), 0);
+ assertEquals(savedPayment.getPaymentStatus(), PaymentStatus.UNKNOWN);
+
+ final List<PaymentAttemptModelDao> attempts = paymentDao.getAttemptsForPayment(payment.getId(), internalCallContext);
+ assertEquals(attempts.size(), 2);
+ final PaymentAttemptModelDao savedAttempt1 = attempts.get(0);
+ assertEquals(savedAttempt1.getPaymentId(), payment.getId());
+ assertEquals(savedAttempt1.getPaymentMethodId(), paymentMethodId);
+ assertEquals(savedAttempt1.getAccountId(), accountId);
+ assertEquals(savedAttempt1.getInvoiceId(), invoiceId);
+ assertEquals(savedAttempt1.getInvoiceId(), invoiceId);
+ assertEquals(savedAttempt1.getGatewayErrorCode(), null);
+ assertEquals(savedAttempt1.getGatewayErrorMsg(), null);
+ assertEquals(savedAttempt1.getRequestedAmount().compareTo(amount), 0);
+
+ final PaymentAttemptModelDao savedAttempt2 = attempts.get(1);
+ assertEquals(savedAttempt2.getPaymentId(), payment.getId());
+ assertEquals(savedAttempt2.getPaymentMethodId(), newPaymentMethodId);
+ assertEquals(savedAttempt2.getAccountId(), accountId);
+ assertEquals(savedAttempt2.getInvoiceId(), invoiceId);
+ assertEquals(savedAttempt2.getProcessingStatus(), PaymentStatus.UNKNOWN);
+ assertEquals(savedAttempt2.getGatewayErrorCode(), null);
+ assertEquals(savedAttempt2.getGatewayErrorMsg(), null);
+ assertEquals(savedAttempt2.getRequestedAmount().compareTo(newAmount), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testPaymentMethod() {
+
+ final UUID paymentMethodId = UUID.randomUUID();
+ final UUID accountId = UUID.randomUUID();
+ final String pluginName = "nobody";
+ final Boolean isActive = Boolean.TRUE;
+ final String externalPaymentId = UUID.randomUUID().toString();
+
+ final PaymentMethodModelDao method = new PaymentMethodModelDao(paymentMethodId, null, null,
+ accountId, pluginName, isActive);
+
+ PaymentMethodModelDao savedMethod = paymentDao.insertPaymentMethod(method, internalCallContext);
+ assertEquals(savedMethod.getId(), paymentMethodId);
+ assertEquals(savedMethod.getAccountId(), accountId);
+ assertEquals(savedMethod.getPluginName(), pluginName);
+ assertEquals(savedMethod.isActive(), isActive);
+
+ final List<PaymentMethodModelDao> result = paymentDao.getPaymentMethods(accountId, internalCallContext);
+ assertEquals(result.size(), 1);
+ savedMethod = result.get(0);
+ assertEquals(savedMethod.getId(), paymentMethodId);
+ assertEquals(savedMethod.getAccountId(), accountId);
+ assertEquals(savedMethod.getPluginName(), pluginName);
+ assertEquals(savedMethod.isActive(), isActive);
+
+ paymentDao.deletedPaymentMethod(paymentMethodId, internalCallContext);
+
+ PaymentMethodModelDao deletedPaymentMethod = paymentDao.getPaymentMethod(paymentMethodId, internalCallContext);
+ assertNull(deletedPaymentMethod);
+
+ deletedPaymentMethod = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodId, internalCallContext);
+ assertNotNull(deletedPaymentMethod);
+ assertFalse(deletedPaymentMethod.isActive());
+ assertEquals(deletedPaymentMethod.getAccountId(), accountId);
+ assertEquals(deletedPaymentMethod.getId(), paymentMethodId);
+ assertEquals(deletedPaymentMethod.getPluginName(), pluginName);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java b/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
new file mode 100644
index 0000000..a95ddc3
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dispatcher;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.payment.api.PaymentApiException;
+
+public class TestPluginDispatcher extends PaymentTestSuiteNoDB {
+
+ private final PluginDispatcher<Void> voidPluginDispatcher = new PluginDispatcher<Void>(10, Executors.newSingleThreadExecutor());
+
+ @Test(groups = "fast")
+ public void testDispatchWithTimeout() throws TimeoutException, PaymentApiException {
+ boolean gotIt = false;
+ try {
+ voidPluginDispatcher.dispatchWithAccountLockAndTimeout(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ Thread.sleep(1000);
+ return null;
+ }
+ }, 100, TimeUnit.MILLISECONDS);
+ Assert.fail("Failed : should have had Timeout exception");
+ } catch (TimeoutException e) {
+ gotIt = true;
+ } catch (PaymentApiException e) {
+ Assert.fail("Failed : should have had Timeout exception");
+ }
+ Assert.assertTrue(gotIt);
+ }
+
+ @Test(groups = "fast")
+ public void testDispatchWithPaymentApiException() throws TimeoutException, PaymentApiException {
+ boolean gotIt = false;
+ try {
+ voidPluginDispatcher.dispatchWithAccountLockAndTimeout(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ throw new PaymentApiException(ErrorCode.PAYMENT_ADD_PAYMENT_METHOD, "foo", "foo");
+ }
+ }, 100, TimeUnit.MILLISECONDS);
+ Assert.fail("Failed : should have had Timeout exception");
+ } catch (TimeoutException e) {
+ Assert.fail("Failed : should have had PaymentApiException exception");
+ } catch (PaymentApiException e) {
+ gotIt = true;
+ }
+ Assert.assertTrue(gotIt);
+ }
+
+ @Test(groups = "fast")
+ public void testDispatchWithRuntimeExceptionWrappedInPaymentApiException() throws TimeoutException, PaymentApiException {
+ boolean gotIt = false;
+ try {
+ voidPluginDispatcher.dispatchWithAccountLockAndTimeout(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ throw new RuntimeException("whatever");
+ }
+ }, 100, TimeUnit.MILLISECONDS);
+ Assert.fail("Failed : should have had Timeout exception");
+ } catch (TimeoutException e) {
+ Assert.fail("Failed : should have had RuntimeException exception");
+ } catch (PaymentApiException e) {
+ gotIt = true;
+ } catch (RuntimeException e) {
+ }
+ Assert.assertTrue(gotIt);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/glue/TestPaymentModule.java b/payment/src/test/java/org/killbill/billing/payment/glue/TestPaymentModule.java
new file mode 100644
index 0000000..2d8d3e7
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/glue/TestPaymentModule.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.glue;
+
+import java.util.UUID;
+
+import org.killbill.billing.util.glue.MemoryGlobalLockerModule;
+import org.mockito.Mockito;
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.mock.glue.MockAccountModule;
+import org.killbill.billing.mock.glue.MockSubscriptionModule;
+import org.killbill.billing.mock.glue.MockInvoiceModule;
+import org.killbill.billing.mock.glue.MockNotificationQueueModule;
+import org.killbill.billing.payment.TestPaymentHelper;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.killbill.billing.payment.provider.MockPaymentProviderPluginModule;
+import org.killbill.billing.util.bus.InMemoryBusModule;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.util.config.PaymentConfig;
+import org.killbill.billing.util.glue.CacheModule;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.tag.Tag;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestPaymentModule extends PaymentModule {
+
+ private final Clock clock;
+
+ public TestPaymentModule(final ConfigSource configSource, final Clock clock) {
+ super(configSource);
+ this.clock = clock;
+ }
+
+ @Override
+ protected void installPaymentProviderPlugins(final PaymentConfig config) {
+ install(new MockPaymentProviderPluginModule(MockPaymentProviderPlugin.PLUGIN_NAME, clock));
+ }
+
+ private void installExternalApis() {
+ final TagInternalApi tagUserApi = Mockito.mock(TagInternalApi.class);
+ bind(TagInternalApi.class).toInstance(tagUserApi);
+ Mockito.when(tagUserApi.getTags(Mockito.<UUID>any(), Mockito.<ObjectType>any(), Mockito.<InternalTenantContext>any())).thenReturn(ImmutableList.<Tag>of());
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ install(new InMemoryBusModule(configSource));
+ install(new MockNotificationQueueModule(configSource));
+ install(new MockInvoiceModule());
+ install(new MockAccountModule());
+ install(new MockSubscriptionModule());
+ install(new MemoryGlobalLockerModule());
+ install(new CacheModule(configSource));
+ installExternalApis();
+
+ bind(TestPaymentHelper.class).asEagerSingleton();
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/glue/TestPaymentModuleNoDB.java b/payment/src/test/java/org/killbill/billing/payment/glue/TestPaymentModuleNoDB.java
new file mode 100644
index 0000000..9a6f6c5
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/glue/TestPaymentModuleNoDB.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+import org.killbill.billing.mock.glue.MockNonEntityDaoModule;
+import org.killbill.billing.payment.dao.MockPaymentDao;
+import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.clock.Clock;
+
+public class TestPaymentModuleNoDB extends TestPaymentModule {
+
+ public TestPaymentModuleNoDB(final ConfigSource configSource, final Clock clock) {
+ super(configSource, clock);
+ }
+
+ @Override
+ protected void installPaymentDao() {
+ bind(PaymentDao.class).to(MockPaymentDao.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ install(new GuicyKillbillTestNoDBModule());
+ install(new MockNonEntityDaoModule());
+ super.configure();
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/glue/TestPaymentModuleWithEmbeddedDB.java b/payment/src/test/java/org/killbill/billing/payment/glue/TestPaymentModuleWithEmbeddedDB.java
new file mode 100644
index 0000000..2a9495c
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/glue/TestPaymentModuleWithEmbeddedDB.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
+import org.killbill.clock.Clock;
+import org.killbill.billing.util.glue.NonEntityDaoModule;
+
+public class TestPaymentModuleWithEmbeddedDB extends TestPaymentModule {
+
+ public TestPaymentModuleWithEmbeddedDB(final ConfigSource configSource, final Clock clock) {
+ super(configSource, clock);
+ }
+
+ @Override
+ protected void configure() {
+ install(new GuicyKillbillTestWithEmbeddedDBModule());
+ install(new NonEntityDaoModule());
+ super.configure();
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/MockInvoice.java b/payment/src/test/java/org/killbill/billing/payment/MockInvoice.java
new file mode 100644
index 0000000..981e974
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/MockInvoice.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+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.InvoicePayment;
+import org.killbill.billing.entity.EntityBase;
+
+public class MockInvoice extends EntityBase implements Invoice {
+ private final List<InvoiceItem> invoiceItems = new ArrayList<InvoiceItem>();
+ private final List<InvoicePayment> payments = new ArrayList<InvoicePayment>();
+ private final UUID accountId;
+ private final Integer invoiceNumber;
+ private final LocalDate invoiceDate;
+ private final LocalDate targetDate;
+ private final Currency currency;
+ private final boolean migrationInvoice;
+
+ // used to create a new invoice
+ public MockInvoice(final UUID accountId, final LocalDate invoiceDate, final LocalDate targetDate, final Currency currency) {
+ this(UUID.randomUUID(), accountId, null, invoiceDate, targetDate, currency, false);
+ }
+
+ // used to hydrate invoice from persistence layer
+ public MockInvoice(final UUID invoiceId, final UUID accountId, @Nullable final Integer invoiceNumber, final LocalDate invoiceDate,
+ final LocalDate targetDate, final Currency currency, final boolean isMigrationInvoice) {
+ super(invoiceId);
+ this.accountId = accountId;
+ this.invoiceNumber = invoiceNumber;
+ this.invoiceDate = invoiceDate;
+ this.targetDate = targetDate;
+ this.currency = currency;
+ this.migrationInvoice = isMigrationInvoice;
+ }
+
+ @Override
+ public boolean addInvoiceItem(final InvoiceItem item) {
+ return invoiceItems.add(item);
+ }
+
+ @Override
+ public boolean addInvoiceItems(final Collection<InvoiceItem> items) {
+ return this.invoiceItems.addAll(items);
+ }
+
+ @Override
+ public List<InvoiceItem> getInvoiceItems() {
+ return invoiceItems;
+ }
+
+ @Override
+ public <T extends InvoiceItem> List<InvoiceItem> getInvoiceItems(final Class<T> clazz) {
+ final List<InvoiceItem> results = new ArrayList<InvoiceItem>();
+ for (final InvoiceItem item : invoiceItems) {
+ if (clazz.isInstance(item)) {
+ results.add(item);
+ }
+ }
+ return results;
+ }
+
+ @Override
+ public int getNumberOfItems() {
+ return invoiceItems.size();
+ }
+
+ @Override
+ public boolean addPayment(final InvoicePayment payment) {
+ return payments.add(payment);
+ }
+
+ @Override
+ public boolean addPayments(final Collection<InvoicePayment> payments) {
+ return this.payments.addAll(payments);
+ }
+
+ @Override
+ public List<InvoicePayment> getPayments() {
+ return payments;
+ }
+
+ @Override
+ public int getNumberOfPayments() {
+ return payments.size();
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ /**
+ * null until retrieved from the database
+ *
+ * @return the invoice number
+ */
+ @Override
+ public Integer getInvoiceNumber() {
+ return invoiceNumber;
+ }
+
+ @Override
+ public LocalDate getInvoiceDate() {
+ return invoiceDate;
+ }
+
+ @Override
+ public LocalDate getTargetDate() {
+ return targetDate;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public boolean isMigrationInvoice() {
+ return migrationInvoice;
+ }
+
+ @Override
+ public BigDecimal getPaidAmount() {
+ BigDecimal amountPaid = BigDecimal.ZERO;
+ for (final InvoicePayment payment : payments) {
+ if (payment.getAmount() != null) {
+ amountPaid = amountPaid.add(payment.getAmount());
+ }
+ }
+ return amountPaid;
+ }
+
+
+ @Override
+ public BigDecimal getChargedAmount() {
+ BigDecimal result = BigDecimal.ZERO;
+
+ for (final InvoiceItem i : invoiceItems) {
+ if (!i.getInvoiceItemType().equals(InvoiceItemType.CBA_ADJ)) {
+ result = result.add(i.getAmount());
+ }
+ }
+ return result;
+ }
+
+
+ @Override
+ public BigDecimal getCreditedAmount() {
+ BigDecimal result = BigDecimal.ZERO;
+
+ for (final InvoiceItem i : invoiceItems) {
+ if (i.getInvoiceItemType().equals(InvoiceItemType.CBA_ADJ)) {
+ result = result.add(i.getAmount());
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public BigDecimal getBalance() {
+ return getChargedAmount().subtract(getPaidAmount());
+ }
+
+ @Override
+ public String toString() {
+ return "DefaultInvoice [items=" + invoiceItems + ", payments=" + payments + ", id=" + id + ", accountId=" + accountId + ", invoiceDate=" + invoiceDate + ", targetDate=" + targetDate + ", currency=" + currency + ", amountPaid=" + getPaidAmount() + "]";
+ }
+
+ @Override
+ public BigDecimal getRefundedAmount() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BigDecimal getOriginalChargedAmount() {
+ throw new UnsupportedOperationException();
+ }
+}
+
diff --git a/payment/src/test/java/org/killbill/billing/payment/MockInvoiceCreationEvent.java b/payment/src/test/java/org/killbill/billing/payment/MockInvoiceCreationEvent.java
new file mode 100644
index 0000000..87df55b
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/MockInvoiceCreationEvent.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.InvoiceCreationInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class MockInvoiceCreationEvent extends BusEventBase implements InvoiceCreationInternalEvent {
+
+ private final UUID invoiceId;
+ private final UUID accountId;
+ private final BigDecimal amountOwed;
+ private final Currency currency;
+ private final LocalDate invoiceCreationDate;
+
+ @JsonCreator
+ public MockInvoiceCreationEvent(@JsonProperty("invoiceId") final UUID invoiceId,
+ @JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("amountOwed") final BigDecimal amountOwed,
+ @JsonProperty("currency") final Currency currency,
+ @JsonProperty("invoiceCreationDate") final LocalDate invoiceCreationDate,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.invoiceId = invoiceId;
+ this.accountId = accountId;
+ this.amountOwed = amountOwed;
+ this.currency = currency;
+ this.invoiceCreationDate = invoiceCreationDate;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.INVOICE_CREATION;
+ }
+
+
+ @Override
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public BigDecimal getAmountOwed() {
+ return amountOwed;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public String toString() {
+ return "DefaultInvoiceCreationNotification [invoiceId=" + invoiceId + ", accountId=" + accountId + ", amountOwed=" + amountOwed + ", currency=" + currency + ", invoiceCreationDate=" + invoiceCreationDate + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((accountId == null) ? 0 : accountId.hashCode());
+ result = prime * result
+ + ((amountOwed == null) ? 0 : amountOwed.hashCode());
+ result = prime * result
+ + ((currency == null) ? 0 : currency.hashCode());
+ result = prime
+ * result
+ + ((invoiceCreationDate == null) ? 0 : invoiceCreationDate
+ .hashCode());
+ result = prime * result
+ + ((invoiceId == null) ? 0 : invoiceId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final MockInvoiceCreationEvent other = (MockInvoiceCreationEvent) obj;
+ if (accountId == null) {
+ if (other.accountId != null) {
+ return false;
+ }
+ } else if (!accountId.equals(other.accountId)) {
+ return false;
+ }
+ if (amountOwed == null) {
+ if (other.amountOwed != null) {
+ return false;
+ }
+ } else if (!amountOwed.equals(other.amountOwed)) {
+ return false;
+ }
+ if (currency != other.currency) {
+ return false;
+ }
+ if (invoiceCreationDate == null) {
+ if (other.invoiceCreationDate != null) {
+ return false;
+ }
+ } else if (invoiceCreationDate.compareTo(other.invoiceCreationDate) != 0) {
+ return false;
+ }
+ if (invoiceId == null) {
+ if (other.invoiceId != null) {
+ return false;
+ }
+ } else if (!invoiceId.equals(other.invoiceId)) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/MockRecurringInvoiceItem.java b/payment/src/test/java/org/killbill/billing/payment/MockRecurringInvoiceItem.java
new file mode 100644
index 0000000..864b402
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/MockRecurringInvoiceItem.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.LocalDate;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.entity.EntityBase;
+
+public class MockRecurringInvoiceItem extends EntityBase implements InvoiceItem {
+ private final BigDecimal rate;
+ private final UUID reversedItemId;
+ protected final UUID invoiceId;
+ protected final UUID accountId;
+ protected final UUID subscriptionId;
+ protected final UUID bundleId;
+ protected final String planName;
+ protected final String phaseName;
+ protected final LocalDate startDate;
+ protected final LocalDate endDate;
+ protected final BigDecimal amount;
+ protected final Currency currency;
+
+ public MockRecurringInvoiceItem(final UUID invoiceId, final UUID accountId, final UUID bundleId, final UUID subscriptionId,
+ final String planName, final String phaseName, final LocalDate startDate, final LocalDate endDate,
+ final BigDecimal amount, final BigDecimal rate, final Currency currency) {
+ this(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, currency, rate, null);
+ }
+
+ public MockRecurringInvoiceItem(final UUID invoiceId, final UUID accountId, final UUID bundleId, final UUID subscriptionId,
+ final String planName, final String phaseName, final LocalDate startDate, final LocalDate endDate,
+ final BigDecimal amount, final BigDecimal rate, final Currency currency, final UUID reversedItemId) {
+ this(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate,
+ amount, currency, rate, reversedItemId);
+ }
+
+ public MockRecurringInvoiceItem(final UUID id, final UUID invoiceId, final UUID accountId, final UUID bundleId,
+ final UUID subscriptionId, final String planName, final String phaseName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount,
+ final BigDecimal rate, final Currency currency) {
+ this(id, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, currency, rate, null);
+
+ }
+
+ public MockRecurringInvoiceItem(final UUID id, final UUID invoiceId, final UUID accountId, final UUID bundleId,
+ final UUID subscriptionId, final String planName, final String phaseName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount,
+ final BigDecimal rate, final Currency currency, final UUID reversedItemId) {
+ this(id, invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, amount, currency, rate, reversedItemId);
+ }
+
+ public MockRecurringInvoiceItem(final UUID invoiceId, final UUID accountId, final UUID bundleId, final UUID subscriptionId, final String planName, final String phaseName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency, final BigDecimal rate, final UUID reversedItemId) {
+ this(UUID.randomUUID(), invoiceId, accountId, bundleId, subscriptionId, planName, phaseName,
+ startDate, endDate, amount, currency, rate, reversedItemId);
+ }
+
+ public MockRecurringInvoiceItem(final UUID id, final UUID invoiceId, final UUID accountId, @Nullable final UUID bundleId, @Nullable final UUID subscriptionId, final String planName, final String phaseName,
+ final LocalDate startDate, final LocalDate endDate, final BigDecimal amount, final Currency currency,
+ final BigDecimal rate, final UUID reversedItemId) {
+ super(id);
+ this.invoiceId = invoiceId;
+ this.accountId = accountId;
+ this.subscriptionId = subscriptionId;
+ this.bundleId = bundleId;
+ this.planName = planName;
+ this.phaseName = phaseName;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.amount = amount;
+ this.currency = currency;
+ this.rate = rate;
+ this.reversedItemId = reversedItemId;
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ @Override
+ public String getPlanName() {
+ return planName;
+ }
+
+ @Override
+ public String getPhaseName() {
+ return phaseName;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public LocalDate getStartDate() {
+ return startDate;
+ }
+
+ @Override
+ public LocalDate getEndDate() {
+ return endDate;
+ }
+
+ @Override
+ public Currency getCurrency() {
+ return currency;
+ }
+
+ @Override
+ public InvoiceItemType getInvoiceItemType() {
+ return InvoiceItemType.RECURRING;
+ }
+
+ @Override
+ public String getDescription() {
+ return String.format("%s from %s to %s", phaseName, startDate.toString(), endDate.toString());
+ }
+
+ @Override
+ public UUID getLinkedItemId() {
+ return reversedItemId;
+ }
+
+ @Override
+ public boolean matches(final Object other) {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean reversesItem() {
+ return (reversedItemId != null);
+ }
+
+ @Override
+ public BigDecimal getRate() {
+ return rate;
+ }
+
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+
+ sb.append(phaseName).append(", ");
+ sb.append(startDate.toString()).append(", ");
+ sb.append(endDate.toString()).append(", ");
+ sb.append(amount.toString()).append(", ");
+ sb.append("subscriptionId = ").append(subscriptionId == null ? null : subscriptionId.toString()).append(", ");
+ sb.append("bundleId = ").append(bundleId == null ? null : bundleId.toString()).append(", ");
+
+ return sb.toString();
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
new file mode 100644
index 0000000..ebf6540
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteNoDB.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment;
+
+import java.net.URL;
+
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.core.PaymentMethodProcessor;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.payment.glue.TestPaymentModuleNoDB;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.killbill.billing.payment.retry.FailedPaymentRetryService;
+import org.killbill.billing.payment.retry.PluginFailureRetryService;
+import org.killbill.billing.util.config.PaymentConfig;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public abstract class PaymentTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ @Inject
+ protected PaymentConfig paymentConfig;
+ @Inject
+ protected PaymentProcessor paymentProcessor;
+ @Inject
+ protected PaymentMethodProcessor paymentMethodProcessor;
+ @Inject
+ protected InvoiceInternalApi invoiceApi;
+ @Inject
+ protected OSGIServiceRegistration<PaymentPluginApi> registry;
+ @Inject
+ protected FailedPaymentRetryService retryService;
+ @Inject
+ protected PluginFailureRetryService pluginRetryService;
+ @Inject
+ protected PersistentBus eventBus;
+ @Inject
+ protected PaymentApi paymentApi;
+ @Inject
+ protected AccountInternalApi accountApi;
+ @Inject
+ protected TestPaymentHelper testHelper;
+
+ private void loadSystemPropertiesFromClasspath(final String resource) {
+ final URL url = PaymentTestSuiteNoDB.class.getResource(resource);
+ Assert.assertNotNull(url);
+
+ configSource.merge(url);
+ configSource.setProperty("org.killbill.payment.provider.default", MockPaymentProviderPlugin.PLUGIN_NAME);
+ configSource.setProperty("killbill.payment.engine.events.off", "false");
+ }
+
+ @BeforeClass(groups = "fast")
+ protected void beforeClass() throws Exception {
+ loadSystemPropertiesFromClasspath("/resource.properties");
+
+ final Injector injector = Guice.createInjector(new TestPaymentModuleNoDB(configSource, getClock()));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ eventBus.start();
+ }
+
+ @AfterMethod(groups = "fast")
+ public void afterMethod() throws Exception {
+ eventBus.stop();
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..3c2330f
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment;
+
+import java.net.URL;
+
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.core.PaymentMethodProcessor;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.billing.payment.glue.TestPaymentModuleWithEmbeddedDB;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.killbill.billing.payment.retry.FailedPaymentRetryService;
+import org.killbill.billing.payment.retry.PluginFailureRetryService;
+import org.killbill.billing.util.config.PaymentConfig;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
+
+ @Inject
+ protected PaymentConfig paymentConfig;
+ @Inject
+ protected PaymentProcessor paymentProcessor;
+ @Inject
+ protected PaymentMethodProcessor paymentMethodProcessor;
+ @Inject
+ protected InvoiceInternalApi invoiceApi;
+ @Inject
+ protected OSGIServiceRegistration<PaymentPluginApi> registry;
+ @Inject
+ protected FailedPaymentRetryService retryService;
+ @Inject
+ protected PluginFailureRetryService pluginRetryService;
+ @Inject
+ protected PersistentBus eventBus;
+ @Inject
+ protected PaymentApi paymentApi;
+ @Inject
+ protected AccountInternalApi accountApi;
+ @Inject
+ protected PaymentDao paymentDao;
+ @Inject
+ protected TestPaymentHelper testHelper;
+
+ private void loadSystemPropertiesFromClasspath(final String resource) {
+ final URL url = PaymentTestSuiteNoDB.class.getResource(resource);
+ Assert.assertNotNull(url);
+
+ configSource.merge(url);
+ configSource.setProperty("org.killbill.payment.provider.default", MockPaymentProviderPlugin.PLUGIN_NAME);
+ configSource.setProperty("killbill.payment.engine.events.off", "false");
+ }
+
+ @BeforeClass(groups = "slow")
+ protected void beforeClass() throws Exception {
+ loadSystemPropertiesFromClasspath("/resource.properties");
+
+ final Injector injector = Guice.createInjector(new TestPaymentModuleWithEmbeddedDB(configSource, getClock()));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ eventBus.start();
+ }
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ eventBus.stop();
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
new file mode 100644
index 0000000..5b98fc5
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.clock.Clock;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+import org.killbill.billing.payment.api.TestPaymentMethodPlugin;
+import org.killbill.billing.payment.plugin.api.NoOpPaymentPluginApi;
+import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.plugin.api.RefundInfoPlugin;
+import org.killbill.billing.payment.plugin.api.RefundPluginStatus;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.DefaultPagination;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
+
+/**
+ * This MockPaymentProviderPlugin only works for a single accounts as we don't specify the accountId
+ * for opeartions such as addPaymentMethod.
+ */
+public class MockPaymentProviderPlugin implements NoOpPaymentPluginApi {
+
+ public static final String PLUGIN_NAME = "__NO_OP__";
+
+ private final AtomicBoolean makeNextInvoiceFailWithError = new AtomicBoolean(false);
+ private final AtomicBoolean makeNextInvoiceFailWithException = new AtomicBoolean(false);
+ private final AtomicBoolean makeAllInvoicesFailWithError = new AtomicBoolean(false);
+
+ private final Map<String, PaymentInfoPlugin> payments = new ConcurrentHashMap<String, PaymentInfoPlugin>();
+ // Note: we can't use HashMultiMap as we care about storing duplicate key/value pairs
+ private final Multimap<String, RefundInfoPlugin> refunds = LinkedListMultimap.<String, RefundInfoPlugin>create();
+ private final Map<String, PaymentMethodPlugin> paymentMethods = new ConcurrentHashMap<String, PaymentMethodPlugin>();
+ private final Map<String, PaymentMethodInfoPlugin> paymentMethodsInfo = new ConcurrentHashMap<String, PaymentMethodInfoPlugin>();
+
+ private final Clock clock;
+
+ @Inject
+ public MockPaymentProviderPlugin(final Clock clock) {
+ this.clock = clock;
+ clear();
+ }
+
+ @Override
+ public void clear() {
+ makeNextInvoiceFailWithException.set(false);
+ makeAllInvoicesFailWithError.set(false);
+ makeNextInvoiceFailWithError.set(false);
+ }
+
+ @Override
+ public void makeNextPaymentFailWithError() {
+ makeNextInvoiceFailWithError.set(true);
+ }
+
+ @Override
+ public void makeNextPaymentFailWithException() {
+ makeNextInvoiceFailWithException.set(true);
+ }
+
+ @Override
+ public void makeAllInvoicesFailWithError(final boolean failure) {
+ makeAllInvoicesFailWithError.set(failure);
+ }
+
+ @Override
+ public PaymentInfoPlugin processPayment(final UUID kbAccountId, final UUID kbPaymentId, final UUID kbPaymentMethodId, final BigDecimal amount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+ if (makeNextInvoiceFailWithException.getAndSet(false)) {
+ throw new PaymentPluginApiException("", "test error");
+ }
+
+ final PaymentPluginStatus status = (makeAllInvoicesFailWithError.get() || makeNextInvoiceFailWithError.getAndSet(false)) ? PaymentPluginStatus.ERROR : PaymentPluginStatus.PROCESSED;
+ final PaymentInfoPlugin result = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, amount, currency, clock.getUTCNow(), clock.getUTCNow(), status, null);
+ payments.put(kbPaymentId.toString(), result);
+ return result;
+ }
+
+ @Override
+ public PaymentInfoPlugin getPaymentInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) throws PaymentPluginApiException {
+ final PaymentInfoPlugin payment = payments.get(kbPaymentId.toString());
+ if (payment == null) {
+ throw new PaymentPluginApiException("", "No payment found for payment id " + kbPaymentId.toString());
+ }
+ return payment;
+ }
+
+ @Override
+ public Pagination<PaymentInfoPlugin> searchPayments(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ final ImmutableList<PaymentInfoPlugin> results = ImmutableList.<PaymentInfoPlugin>copyOf(Iterables.<PaymentInfoPlugin>filter(payments.values(), new Predicate<PaymentInfoPlugin>() {
+ @Override
+ public boolean apply(final PaymentInfoPlugin input) {
+ return (input.getKbPaymentId() != null && input.getKbPaymentId().toString().equals(searchKey)) ||
+ (input.getFirstPaymentReferenceId() != null && input.getFirstPaymentReferenceId().contains(searchKey)) ||
+ (input.getSecondPaymentReferenceId() != null && input.getSecondPaymentReferenceId().contains(searchKey));
+ }
+ }));
+ return DefaultPagination.<PaymentInfoPlugin>build(offset, limit, results);
+ }
+
+ @Override
+ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final CallContext context) throws PaymentPluginApiException {
+ // externalPaymentMethodId is set to a random value
+ final PaymentMethodPlugin realWithID = new TestPaymentMethodPlugin(kbPaymentMethodId, paymentMethodProps, UUID.randomUUID().toString());
+ paymentMethods.put(kbPaymentMethodId.toString(), realWithID);
+
+ final PaymentMethodInfoPlugin realInfoWithID = new DefaultPaymentMethodInfoPlugin(kbAccountId, kbPaymentMethodId, setDefault, UUID.randomUUID().toString());
+ paymentMethodsInfo.put(kbPaymentMethodId.toString(), realInfoWithID);
+ }
+
+ @Override
+ public void deletePaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+ paymentMethods.remove(kbPaymentMethodId.toString());
+ paymentMethodsInfo.remove(kbPaymentMethodId.toString());
+ }
+
+ @Override
+ public PaymentMethodPlugin getPaymentMethodDetail(final UUID kbAccountId, final UUID kbPaymentMethodId, final TenantContext context) throws PaymentPluginApiException {
+ return paymentMethods.get(kbPaymentMethodId.toString());
+ }
+
+ @Override
+ public void setDefaultPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final CallContext context) throws PaymentPluginApiException {
+ }
+
+ @Override
+ public List<PaymentMethodInfoPlugin> getPaymentMethods(final UUID kbAccountId, final boolean refreshFromGateway, final CallContext context) {
+ return ImmutableList.<PaymentMethodInfoPlugin>copyOf(paymentMethodsInfo.values());
+ }
+
+ @Override
+ public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ final ImmutableList<PaymentMethodPlugin> results = ImmutableList.<PaymentMethodPlugin>copyOf(Iterables.<PaymentMethodPlugin>filter(paymentMethods.values(), new Predicate<PaymentMethodPlugin>() {
+ @Override
+ public boolean apply(final PaymentMethodPlugin input) {
+ return (input.getAddress1() != null && input.getAddress1().contains(searchKey)) ||
+ (input.getAddress2() != null && input.getAddress2().contains(searchKey)) ||
+ (input.getCCLast4() != null && input.getCCLast4().contains(searchKey)) ||
+ (input.getCCName() != null && input.getCCName().contains(searchKey)) ||
+ (input.getCity() != null && input.getCity().contains(searchKey)) ||
+ (input.getState() != null && input.getState().contains(searchKey)) ||
+ (input.getCountry() != null && input.getCountry().contains(searchKey));
+ }
+ }));
+ return DefaultPagination.<PaymentMethodPlugin>build(offset, limit, results);
+ }
+
+ @Override
+ public void resetPaymentMethods(final UUID kbAccountId, final List<PaymentMethodInfoPlugin> input) {
+ paymentMethodsInfo.clear();
+ if (input != null) {
+ for (final PaymentMethodInfoPlugin cur : input) {
+ paymentMethodsInfo.put(cur.getPaymentMethodId().toString(), cur);
+ }
+ }
+ }
+
+ @Override
+ public RefundInfoPlugin processRefund(final UUID kbAccountId, final UUID kbPaymentId, final BigDecimal refundAmount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
+ final PaymentInfoPlugin paymentInfoPlugin = getPaymentInfo(kbAccountId, kbPaymentId, context);
+ if (paymentInfoPlugin == null) {
+ throw new PaymentPluginApiException("", String.format("No payment found for payment id %s (plugin %s)", kbPaymentId.toString(), PLUGIN_NAME));
+ }
+
+ BigDecimal maxAmountRefundable = paymentInfoPlugin.getAmount();
+ for (final RefundInfoPlugin refund : refunds.get(kbPaymentId.toString())) {
+ maxAmountRefundable = maxAmountRefundable.add(refund.getAmount().negate());
+ }
+ if (maxAmountRefundable.compareTo(refundAmount) < 0) {
+ throw new PaymentPluginApiException("", String.format("Refund amount of %s for payment id %s is bigger than the payment amount %s (plugin %s)",
+ refundAmount, kbPaymentId.toString(), paymentInfoPlugin.getAmount(), PLUGIN_NAME));
+ }
+
+ final DefaultNoOpRefundInfoPlugin refundInfoPlugin = new DefaultNoOpRefundInfoPlugin(kbPaymentId, refundAmount, currency, clock.getUTCNow(), clock.getUTCNow(), RefundPluginStatus.PROCESSED, null);
+ refunds.put(kbPaymentId.toString(), refundInfoPlugin);
+
+ return refundInfoPlugin;
+ }
+
+ @Override
+ public List<RefundInfoPlugin> getRefundInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) throws PaymentPluginApiException {
+ return Collections.<RefundInfoPlugin>emptyList();
+ }
+
+ @Override
+ public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+ final ImmutableList<RefundInfoPlugin> results = ImmutableList.<RefundInfoPlugin>copyOf(Iterables.<RefundInfoPlugin>filter(refunds.values(), new Predicate<RefundInfoPlugin>() {
+ @Override
+ public boolean apply(final RefundInfoPlugin input) {
+ return (input.getKbPaymentId() != null && input.getKbPaymentId().toString().equals(searchKey)) ||
+ (input.getFirstRefundReferenceId() != null && input.getFirstRefundReferenceId().contains(searchKey)) ||
+ (input.getSecondRefundReferenceId() != null && input.getSecondRefundReferenceId().contains(searchKey));
+ }
+ }));
+ return DefaultPagination.<RefundInfoPlugin>build(offset, limit, results);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPluginModule.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPluginModule.java
new file mode 100644
index 0000000..2edf947
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPluginModule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import org.killbill.clock.Clock;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+public class MockPaymentProviderPluginModule extends AbstractModule {
+
+ private final String instanceName;
+ private final Clock clock;
+
+ public MockPaymentProviderPluginModule(final String instanceName, final Clock clock) {
+ this.instanceName = instanceName;
+ this.clock = clock;
+ }
+
+ @Override
+ protected void configure() {
+ bind(MockPaymentProviderPlugin.class)
+ .annotatedWith(Names.named(instanceName))
+ .toProvider(new MockPaymentProviderPluginProvider(instanceName, clock))
+ .asEagerSingleton();
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPluginProvider.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPluginProvider.java
new file mode 100644
index 0000000..f235ba6
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPluginProvider.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.clock.Clock;
+
+public class MockPaymentProviderPluginProvider implements Provider<MockPaymentProviderPlugin> {
+
+ private OSGIServiceRegistration<PaymentPluginApi> registry;
+ private final String instanceName;
+
+ private Clock clock;
+
+ public MockPaymentProviderPluginProvider(final String instanceName, Clock clock) {
+ this.instanceName = instanceName;
+ this.clock = clock;
+ }
+
+ @Inject
+ public void setPaymentProviderPluginRegistry(final OSGIServiceRegistration<PaymentPluginApi> registry) {
+ this.registry = registry;
+ }
+
+ @Override
+ public MockPaymentProviderPlugin get() {
+ final MockPaymentProviderPlugin plugin = new MockPaymentProviderPlugin(clock);
+
+ final OSGIServiceDescriptor desc = new OSGIServiceDescriptor() {
+ @Override
+ public String getPluginSymbolicName() {
+ return null;
+ }
+ @Override
+ public String getRegistrationName() {
+ return instanceName;
+ }
+ };
+ registry.registerService(desc, plugin);
+ return plugin;
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java
new file mode 100644
index 0000000..b0c0907
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+
+public class TestDefaultNoOpPaymentInfoPlugin extends PaymentTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final UUID kbPaymentId = UUID.randomUUID();
+ final BigDecimal amount = new BigDecimal("1.394810E-3");
+ final DateTime effectiveDate = clock.getUTCNow().plusDays(1);
+ final DateTime createdDate = clock.getUTCNow();
+ final PaymentPluginStatus status = PaymentPluginStatus.UNDEFINED;
+ final String error = UUID.randomUUID().toString();
+
+ final DefaultNoOpPaymentInfoPlugin info = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, amount, Currency.USD, effectiveDate, createdDate,
+ status, error);
+ Assert.assertEquals(info, info);
+
+ final DefaultNoOpPaymentInfoPlugin sameInfo = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, amount, Currency.USD, effectiveDate, createdDate,
+ status, error);
+ Assert.assertEquals(sameInfo, info);
+
+ final DefaultNoOpPaymentInfoPlugin otherInfo = new DefaultNoOpPaymentInfoPlugin(kbPaymentId, amount, Currency.USD, effectiveDate, createdDate,
+ status, UUID.randomUUID().toString());
+ Assert.assertNotEquals(otherInfo, info);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentMethodPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentMethodPlugin.java
new file mode 100644
index 0000000..566cf50
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/TestDefaultNoOpPaymentMethodPlugin.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.payment.api.PaymentMethodKVInfo;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultNoOpPaymentMethodPlugin extends PaymentTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final String externalId = UUID.randomUUID().toString();
+ final boolean isDefault = false;
+ final List<PaymentMethodKVInfo> props = ImmutableList.<PaymentMethodKVInfo>of(new PaymentMethodKVInfo(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false));
+
+ final DefaultNoOpPaymentMethodPlugin paymentMethod = new DefaultNoOpPaymentMethodPlugin(externalId, isDefault, props);
+ Assert.assertEquals(paymentMethod, paymentMethod);
+
+ final DefaultNoOpPaymentMethodPlugin samePaymentMethod = new DefaultNoOpPaymentMethodPlugin(externalId, isDefault, props);
+ Assert.assertEquals(samePaymentMethod, paymentMethod);
+
+ final DefaultNoOpPaymentMethodPlugin otherPaymentMethod = new DefaultNoOpPaymentMethodPlugin(externalId, isDefault, ImmutableList.<PaymentMethodKVInfo>of());
+ Assert.assertNotEquals(otherPaymentMethod, paymentMethod);
+ }
+
+ @Test(groups = "fast")
+ public void testEqualsForPaymentMethodKVInfo() throws Exception {
+ final String key = UUID.randomUUID().toString();
+ final Object value = UUID.randomUUID();
+ final boolean updatable = false;
+
+ final PaymentMethodKVInfo kvInfo = new PaymentMethodKVInfo(key, value, updatable);
+ Assert.assertEquals(kvInfo, kvInfo);
+
+ final PaymentMethodKVInfo sameKvInfo = new PaymentMethodKVInfo(key, value, updatable);
+ Assert.assertEquals(sameKvInfo, kvInfo);
+
+ final PaymentMethodKVInfo otherKvInfo = new PaymentMethodKVInfo(key, value, !updatable);
+ Assert.assertNotEquals(otherKvInfo, kvInfo);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/TestExternalPaymentProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/TestExternalPaymentProviderPlugin.java
new file mode 100644
index 0000000..fe00cea
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/TestExternalPaymentProviderPlugin.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.provider;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.PaymentTestSuiteNoDB;
+import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
+
+public class TestExternalPaymentProviderPlugin extends PaymentTestSuiteNoDB {
+
+ private final Clock clock = new ClockMock();
+ private ExternalPaymentProviderPlugin plugin;
+
+ @Override
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ plugin = new ExternalPaymentProviderPlugin(clock);
+ }
+
+
+ @Test(groups = "fast")
+ public void testProcessPayment() throws Exception {
+
+ final UUID accountId = UUID.randomUUID();
+ final UUID paymentId = UUID.randomUUID();
+ final UUID paymentMethodId = UUID.randomUUID();
+ final BigDecimal amount = BigDecimal.TEN;
+ final PaymentInfoPlugin paymentInfoPlugin = plugin.processPayment(accountId, paymentId, paymentMethodId, amount, Currency.BRL, callContext);
+
+ Assert.assertEquals(paymentInfoPlugin.getAmount(), amount);
+ Assert.assertNull(paymentInfoPlugin.getGatewayError());
+ Assert.assertNull(paymentInfoPlugin.getGatewayErrorCode());
+ Assert.assertEquals(paymentInfoPlugin.getStatus(), PaymentPluginStatus.PROCESSED);
+
+ final PaymentInfoPlugin retrievedPaymentInfoPlugin = plugin.getPaymentInfo(accountId, paymentId, callContext);
+ Assert.assertEquals(retrievedPaymentInfoPlugin.getStatus(), PaymentPluginStatus.PROCESSED);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestPaymentHelper.java b/payment/src/test/java/org/killbill/billing/payment/TestPaymentHelper.java
new file mode 100644
index 0000000..fb6fffe
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/TestPaymentHelper.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment;
+
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.mockito.Mockito;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.bus.api.PersistentBus.EventBusException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentMethodPlugin;
+import org.killbill.billing.payment.provider.DefaultNoOpPaymentMethodPlugin;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.events.InvoiceCreationInternalEvent;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+
+import com.google.inject.Inject;
+
+public class TestPaymentHelper {
+
+ protected final AccountInternalApi AccountApi;
+ protected final InvoiceInternalApi invoiceApi;
+ protected PaymentApi paymentApi;
+ private final PersistentBus eventBus;
+ private final Clock clock;
+
+ private final CallContext context;
+ private final InternalCallContext internalCallContext;
+
+ @Inject
+ public TestPaymentHelper(final AccountInternalApi AccountApi, final InvoiceInternalApi invoiceApi,
+ final PaymentApi paymentApi, final PersistentBus eventBus, final Clock clock,
+ final CallContext context, final InternalCallContext internalCallContext) {
+ this.eventBus = eventBus;
+ this.AccountApi = AccountApi;
+ this.invoiceApi = invoiceApi;
+ this.paymentApi = paymentApi;
+ this.clock = clock;
+ this.context = context;
+ this.internalCallContext = internalCallContext;
+ }
+
+ public Invoice createTestInvoice(final Account account,
+ final LocalDate targetDate,
+ final Currency currency,
+ final CallContext context,
+ final InvoiceItem... items) throws EventBusException, InvoiceApiException {
+ final Invoice invoice = new MockInvoice(account.getId(), clock.getUTCToday(), targetDate, currency);
+
+ for (final InvoiceItem item : items) {
+ if (item instanceof MockRecurringInvoiceItem) {
+ final MockRecurringInvoiceItem recurringInvoiceItem = (MockRecurringInvoiceItem) item;
+ invoice.addInvoiceItem(new MockRecurringInvoiceItem(invoice.getId(),
+ account.getId(),
+ recurringInvoiceItem.getBundleId(),
+ recurringInvoiceItem.getSubscriptionId(),
+ recurringInvoiceItem.getPlanName(),
+ recurringInvoiceItem.getPhaseName(),
+ recurringInvoiceItem.getStartDate(),
+ recurringInvoiceItem.getEndDate(),
+ recurringInvoiceItem.getAmount(),
+ recurringInvoiceItem.getRate(),
+ recurringInvoiceItem.getCurrency()));
+ }
+ }
+
+ Mockito.when(invoiceApi.getInvoiceById(Mockito.eq(invoice.getId()), Mockito.<InternalTenantContext>any())).thenReturn(invoice);
+ final InvoiceCreationInternalEvent event = new MockInvoiceCreationEvent(invoice.getId(), invoice.getAccountId(),
+ invoice.getBalance(), invoice.getCurrency(),
+ invoice.getInvoiceDate(), 1L, 2L, null);
+
+ eventBus.post(event);
+ return invoice;
+ }
+
+ public Account createTestAccount(final String email, final boolean addPaymentMethod) throws Exception {
+ final String name = "First" + UUID.randomUUID().toString() + " " + "Last" + UUID.randomUUID().toString();
+ final String externalKey = UUID.randomUUID().toString();
+
+ final Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getId()).thenReturn(UUID.randomUUID());
+ Mockito.when(account.getExternalKey()).thenReturn(externalKey);
+ Mockito.when(account.getName()).thenReturn(name);
+ Mockito.when(account.getFirstNameLength()).thenReturn(10);
+ Mockito.when(account.getPhone()).thenReturn("123-456-7890");
+ Mockito.when(account.getEmail()).thenReturn(email);
+ Mockito.when(account.getCurrency()).thenReturn(Currency.USD);
+ Mockito.when(account.getBillCycleDayLocal()).thenReturn(1);
+ Mockito.when(account.isMigrated()).thenReturn(false);
+ Mockito.when(account.isNotifiedForInvoices()).thenReturn(false);
+
+ Mockito.when(AccountApi.getAccountById(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(account);
+ Mockito.when(AccountApi.getAccountByKey(Mockito.anyString(), Mockito.<InternalTenantContext>any())).thenReturn(account);
+
+ if (addPaymentMethod) {
+ final PaymentMethodPlugin pm = new DefaultNoOpPaymentMethodPlugin(UUID.randomUUID().toString(), true, null);
+ addTestPaymentMethod(account, pm);
+ }
+ return account;
+ }
+
+ public void addTestPaymentMethod(final Account account, final PaymentMethodPlugin paymentMethodInfo) throws Exception {
+ final UUID paymentMethodId = paymentApi.addPaymentMethod(MockPaymentProviderPlugin.PLUGIN_NAME, account, true, paymentMethodInfo, context);
+ Mockito.when(account.getPaymentMethodId()).thenReturn(paymentMethodId);
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
new file mode 100644
index 0000000..b64e047
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/TestRetryService.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeoutException;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentAttempt;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentStatus;
+import org.killbill.billing.payment.glue.DefaultPaymentService;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+
+import static com.jayway.awaitility.Awaitility.await;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class TestRetryService extends PaymentTestSuiteNoDB {
+
+ private MockPaymentProviderPlugin mockPaymentProviderPlugin;
+
+ @Override
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ pluginRetryService.initialize(DefaultPaymentService.SERVICE_NAME);
+ pluginRetryService.start();
+
+ retryService.initialize(DefaultPaymentService.SERVICE_NAME);
+ retryService.start();
+
+ mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME);
+ mockPaymentProviderPlugin.clear();
+ }
+
+ @Override
+ @AfterMethod(groups = "fast")
+ public void afterMethod() throws Exception {
+ super.afterMethod();
+ retryService.stop();
+ pluginRetryService.stop();
+ }
+
+ private Payment getPaymentForInvoice(final UUID invoiceId) throws PaymentApiException {
+ final List<Payment> payments = paymentProcessor.getInvoicePayments(invoiceId, internalCallContext);
+ assertEquals(payments.size(), 1);
+ final Payment payment = payments.get(0);
+ assertEquals(payment.getInvoiceId(), invoiceId);
+ return payment;
+ }
+
+ @Test(groups = "fast")
+ public void testFailedPluginWithOneSuccessfulRetry() throws Exception {
+ testSchedulesRetryInternal(1, FailureType.PLUGIN_EXCEPTION);
+ }
+
+ @Test(groups = "fast")
+ public void testFailedPpluginWithLastRetrySuccess() throws Exception {
+ testSchedulesRetryInternal(paymentConfig.getPluginFailureRetryMaxAttempts(), FailureType.PLUGIN_EXCEPTION);
+ }
+
+ @Test(groups = "fast")
+ public void testAbortedPlugin() throws Exception {
+ testSchedulesRetryInternal(paymentConfig.getPluginFailureRetryMaxAttempts() + 1, FailureType.PLUGIN_EXCEPTION);
+ }
+
+ @Test(groups = "fast")
+ public void testFailedPaymentWithOneSuccessfulRetry() throws Exception {
+ testSchedulesRetryInternal(1, FailureType.PAYMENT_FAILURE);
+ }
+
+ @Test(groups = "fast")
+ public void testFailedPaymentWithLastRetrySuccess() throws Exception {
+ testSchedulesRetryInternal(paymentConfig.getPaymentRetryDays().size(), FailureType.PAYMENT_FAILURE);
+ }
+
+ @Test(groups = "fast")
+ public void testAbortedPayment() throws Exception {
+ testSchedulesRetryInternal(paymentConfig.getPaymentRetryDays().size() + 1, FailureType.PAYMENT_FAILURE);
+ }
+
+ private void testSchedulesRetryInternal(final int maxTries, final FailureType failureType) throws Exception {
+
+ final Account account = testHelper.createTestAccount("yiyi.gmail.com", true);
+ final Invoice invoice = testHelper.createTestInvoice(account, clock.getUTCToday(), Currency.USD, callContext);
+ final BigDecimal amount = new BigDecimal("10.00");
+ final UUID subscriptionId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+
+ final LocalDate startDate = clock.getUTCToday();
+ final LocalDate endDate = startDate.plusMonths(1);
+ invoice.addInvoiceItem(new MockRecurringInvoiceItem(invoice.getId(),
+ account.getId(),
+ subscriptionId,
+ bundleId,
+ "test plan", "test phase",
+ startDate,
+ endDate,
+ amount,
+ new BigDecimal("1.0"),
+ Currency.USD));
+ setPaymentFailure(failureType);
+ boolean failed = false;
+ try {
+ paymentProcessor.createPayment(account, invoice.getId(), amount, internalCallContext, false, false);
+ } catch (PaymentApiException e) {
+ failed = true;
+ }
+ assertTrue(failed);
+
+ for (int curFailure = 0; curFailure < maxTries; curFailure++) {
+
+ if (curFailure < maxTries - 1) {
+ setPaymentFailure(failureType);
+ }
+
+ if (curFailure < getMaxRetrySizeForFailureType(failureType)) {
+
+ moveClockForFailureType(failureType, curFailure);
+ try {
+ await().atMost(3, SECONDS).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ final Payment payment = getPaymentForInvoice(invoice.getId());
+ return payment.getPaymentStatus() == PaymentStatus.SUCCESS;
+ }
+ });
+ } catch (TimeoutException e) {
+ if (curFailure == maxTries - 1) {
+ fail("Failed to find successful payment for attempt " + (curFailure + 1) + "/" + maxTries);
+ }
+ }
+ }
+ }
+ final Payment payment = getPaymentForInvoice(invoice.getId());
+ final List<PaymentAttempt> attempts = payment.getAttempts();
+
+ final int expectedAttempts = maxTries < getMaxRetrySizeForFailureType(failureType) ?
+ maxTries + 1 : getMaxRetrySizeForFailureType(failureType) + 1;
+ assertEquals(attempts.size(), expectedAttempts);
+ Collections.sort(attempts, new Comparator<PaymentAttempt>() {
+ @Override
+ public int compare(final PaymentAttempt o1, final PaymentAttempt o2) {
+ return o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
+ }
+ });
+
+ for (int i = 0; i < attempts.size(); i++) {
+ final PaymentAttempt cur = attempts.get(i);
+ if (i < attempts.size() - 1) {
+ if (failureType == FailureType.PAYMENT_FAILURE) {
+ assertEquals(cur.getPaymentStatus(), PaymentStatus.PAYMENT_FAILURE);
+ } else {
+ assertEquals(cur.getPaymentStatus(), PaymentStatus.PLUGIN_FAILURE);
+ }
+ } else if (maxTries <= getMaxRetrySizeForFailureType(failureType)) {
+ assertEquals(cur.getPaymentStatus(), PaymentStatus.SUCCESS);
+ assertEquals(payment.getPaymentStatus(), PaymentStatus.SUCCESS);
+ } else {
+ if (failureType == FailureType.PAYMENT_FAILURE) {
+ assertEquals(cur.getPaymentStatus(), PaymentStatus.PAYMENT_FAILURE_ABORTED);
+ assertEquals(payment.getPaymentStatus(), PaymentStatus.PAYMENT_FAILURE_ABORTED);
+ } else {
+ assertEquals(cur.getPaymentStatus(), PaymentStatus.PLUGIN_FAILURE_ABORTED);
+ assertEquals(payment.getPaymentStatus(), PaymentStatus.PLUGIN_FAILURE_ABORTED);
+ }
+ }
+ }
+ }
+
+ private enum FailureType {
+ PLUGIN_EXCEPTION,
+ PAYMENT_FAILURE
+ }
+
+ private void setPaymentFailure(final FailureType failureType) {
+ if (failureType == FailureType.PAYMENT_FAILURE) {
+ mockPaymentProviderPlugin.makeNextPaymentFailWithError();
+ } else if (failureType == FailureType.PLUGIN_EXCEPTION) {
+ mockPaymentProviderPlugin.makeNextPaymentFailWithException();
+ }
+ }
+
+ private void moveClockForFailureType(final FailureType failureType, final int curFailure) {
+ if (failureType == FailureType.PAYMENT_FAILURE) {
+ final int nbDays = paymentConfig.getPaymentRetryDays().get(curFailure);
+ clock.addDays(nbDays + 1);
+ } else {
+ clock.addDays(1);
+ }
+ }
+
+ private int getMaxRetrySizeForFailureType(final FailureType failureType) {
+ if (failureType == FailureType.PAYMENT_FAILURE) {
+ return paymentConfig.getPaymentRetryDays().size();
+ } else {
+ return paymentConfig.getPluginFailureRetryMaxAttempts();
+ }
+ }
+}
diff --git a/payment/src/test/resources/payment.properties b/payment/src/test/resources/payment.properties
index 5841e2e..41775d7 100644
--- a/payment/src/test/resources/payment.properties
+++ b/payment/src/test/resources/payment.properties
@@ -1,4 +1,4 @@
killbill.payment.failure.retry.start.sec=3600
killbill.payment.failure.retry.multiplier=1
killbill.payment.failure.retry.max.attempts=3
-killbill.billing.persistent.bus.main.claimed=1
+org.killbill.persistent.bus.main.claimed=1
pom.xml 6(+3 -3)
diff --git a/pom.xml b/pom.xml
index 5a30c75..490d912 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,11 +18,11 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill-oss-parent</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.5.24</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.5.28</version>
</parent>
<artifactId>killbill</artifactId>
- <version>0.9.0-SNAPSHOT</version>
+ <version>0.9.2-SNAPSHOT</version>
<packaging>pom</packaging>
<name>killbill</name>
<description>Library for managing recurring subscriptions and the associated billing</description>
server/pom.xml 238(+126 -112)
diff --git a/server/pom.xml b/server/pom.xml
index c363832..7311348 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-server</artifactId>
@@ -41,10 +41,30 @@
<scope>runtime</scope>
</dependency>
<dependency>
+ <groupId>com.codahale.metrics</groupId>
+ <artifactId>metrics-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.codahale.metrics</groupId>
+ <artifactId>metrics-jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.codahale.metrics</groupId>
+ <artifactId>metrics-jersey</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.codahale.metrics</groupId>
+ <artifactId>metrics-servlet</artifactId>
+ </dependency>
+ <dependency>
<groupId>com.dmurph</groupId>
<artifactId>JGoogleAnalyticsTracker</artifactId>
</dependency>
<dependency>
+ <groupId>com.fasterxml.jackson.jaxrs</groupId>
+ <artifactId>jackson-jaxrs-json-provider</artifactId>
+ </dependency>
+ <dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>compile</scope>
@@ -63,207 +83,202 @@
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-servlet</artifactId>
- <version>${guice.version}</version>
+ <scope>compile</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
- <scope>test</scope>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.mchange</groupId>
+ <artifactId>c3p0</artifactId>
</dependency>
<dependency>
<groupId>com.ning</groupId>
<artifactId>async-http-client</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-account</artifactId>
+ <groupId>com.palominolabs.metrics</groupId>
+ <artifactId>metrics-guice</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-api</artifactId>
+ <groupId>com.sun.jersey.contribs</groupId>
+ <artifactId>jersey-guice</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-beatrix</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-beatrix</artifactId>
- <type>test-jar</type>
- <scope>test</scope>
+ <groupId>javax.ws.rs</groupId>
+ <artifactId>jsr311-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-catalog</artifactId>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-client-java</artifactId>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-java</artifactId>
+ <scope>runtime</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-currency</artifactId>
+ <groupId>org.antlr</groupId>
+ <artifactId>stringtemplate</artifactId>
+ <scope>runtime</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-invoice</artifactId>
+ <groupId>org.apache.shiro</groupId>
+ <artifactId>shiro-web</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-jaxrs</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-deploy</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-junction</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-http</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-osgi</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-io</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-overdue</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-jmx</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-payment</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-payment</artifactId>
- <type>test-jar</type>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-util</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-subscription</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-xml</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-tenant</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-account</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-usage</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-util</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-beatrix</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-util</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-beatrix</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-clock</artifactId>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-clock</artifactId>
- <type>test-jar</type>
- <!--
-+ Until we move ClockMock outside of test package
- <scope>test</scope>
- -->
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-catalog</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-queue</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-client-java</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.jetty</groupId>
- <artifactId>ning-service-skeleton-base</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-currency</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.jetty</groupId>
- <artifactId>ning-service-skeleton-jdbi</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-invoice</artifactId>
</dependency>
<dependency>
- <groupId>com.yammer.metrics</groupId>
- <artifactId>metrics-core</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-jaxrs</artifactId>
</dependency>
<dependency>
- <groupId>com.yammer.metrics</groupId>
- <artifactId>metrics-guice</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-junction</artifactId>
</dependency>
<dependency>
- <groupId>javax.servlet</groupId>
- <artifactId>javax.servlet-api</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-osgi</artifactId>
</dependency>
<dependency>
- <groupId>javax.ws.rs</groupId>
- <artifactId>jsr311-api</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-overdue</artifactId>
</dependency>
<dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-payment</artifactId>
</dependency>
<dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- <scope>runtime</scope>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-payment</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-subscription</artifactId>
</dependency>
<dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-tenant</artifactId>
</dependency>
<dependency>
- <groupId>org.antlr</groupId>
- <artifactId>stringtemplate</artifactId>
- <scope>runtime</scope>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-usage</artifactId>
</dependency>
<dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-web</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-deploy</artifactId>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-util</artifactId>
+ <type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-http</artifactId>
- <scope>test</scope>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-io</artifactId>
- <scope>test</scope>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-clock</artifactId>
+ <type>test-jar</type>
+ <!--
++ Until we move ClockMock outside of test package
+ <scope>test</scope>
+ -->
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-jmx</artifactId>
- <scope>test</scope>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-embeddeddb-h2</artifactId>
+ <scope>compile</scope>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-server</artifactId>
- <scope>test</scope>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-embeddeddb-mysql</artifactId>
+ <scope>compile</scope>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-util</artifactId>
- <scope>test</scope>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-xml</artifactId>
- <scope>test</scope>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-skeleton</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
@@ -292,7 +307,6 @@
<dependency>
<groupId>org.weakref</groupId>
<artifactId>jmxutils</artifactId>
- <version>1.12</version>
</dependency>
</dependencies>
<build>
server/src/deb/control/config 6(+3 -3)
diff --git a/server/src/deb/control/config b/server/src/deb/control/config
index becf7d7..efcee41 100755
--- a/server/src/deb/control/config
+++ b/server/src/deb/control/config
@@ -14,9 +14,9 @@ db_input medium killbill/username || true
db_input medium killbill/groupname || true
# Set default values for Kill Bill configuration
-db_set killbill/dburl "$(default com.ning.jetty.jdbi.url)"
-db_set killbill/dbusername "$(default com.ning.jetty.jdbi.user)"
-db_set killbill/dbpassword "$(default com.ning.jetty.jdbi.password)"
+db_set killbill/dburl "$(default org.killbill.jetty.jdbi.url)"
+db_set killbill/dbusername "$(default org.killbill.jetty.jdbi.user)"
+db_set killbill/dbpassword "$(default org.killbill.jetty.jdbi.password)"
db_input medium killbill/dburl || true
db_input medium killbill/dbusername || true
server/src/deb/control/postinst 6(+3 -3)
diff --git a/server/src/deb/control/postinst b/server/src/deb/control/postinst
index ecc1f1a..a9a5271 100755
--- a/server/src/deb/control/postinst
+++ b/server/src/deb/control/postinst
@@ -42,9 +42,9 @@ case "$1" in
chown -R ${KILLBILL_USER}:${KILLBILL_GROUP} ${KILLBILL_HOME} || true
# Configure Kill Bill properties (see config script)
- set_property com.ning.jetty.jdbi.url killbill/dburl
- set_property com.ning.jetty.jdbi.user killbill/dbusername
- set_property com.ning.jetty.jdbi.password killbill/dbpassword
+ set_property org.killbill.jetty.jdbi.url killbill/dburl
+ set_property org.killbill.jetty.jdbi.user killbill/dbusername
+ set_property org.killbill.jetty.jdbi.password killbill/dbpassword
;;
abort-upgrade|abort-remove|abort-deconfigure)
server/src/deb/support/killbill.properties 26(+13 -13)
diff --git a/server/src/deb/support/killbill.properties b/server/src/deb/support/killbill.properties
index 86cc5da..ed16d9b 100644
--- a/server/src/deb/support/killbill.properties
+++ b/server/src/deb/support/killbill.properties
@@ -17,12 +17,12 @@
logback.configurationFile=/etc/killbill/logback.xml
# Use skeleton properties for server and configure killbill database
-com.ning.jetty.jdbi.url=jdbc:mysql://127.0.0.1:3306/killbill
-com.ning.jetty.jdbi.user=root
-com.ning.jetty.jdbi.password=root
+org.killbill.jetty.jdbi.url=jdbc:mysql://127.0.0.1:3306/killbill
+org.killbill.jetty.jdbi.user=root
+org.killbill.jetty.jdbi.password=root
# Use the SpyCarAdvanced.xml catalog
-killbill.catalog.uri=SpyCarAdvanced.xml
+org.killbill.catalog.uri=SpyCarAdvanced.xml
# Set default timezone to UTC
user.timezone=UTC
@@ -30,14 +30,14 @@ user.timezone=UTC
# For bundles that use antlr (string template)
ANTLR_USE_DIRECT_CLASS_LOADING=true
-killbill.billing.notificationq.main.sleep=100
+org.killbill.notificationq.main.sleep=100
-killbill.billing.persistent.bus.main.sleep=100
-killbill.billing.persistent.bus.main.nbThreads=1
-killbill.billing.persistent.bus.main.claimed=1
+org.killbill.persistent.bus.main.sleep=100
+org.killbill.persistent.bus.main.nbThreads=1
+org.killbill.persistent.bus.main.claimed=1
-killbill.billing.persistent.bus.external.sleep=100
-killbill.billing.persistent.bus.external.nbThreads=1
-killbill.billing.persistent.bus.external.claimed=1
-killbill.billing.persistent.bus.external.tableName=bus_ext_events
-killbill.billing.persistent.bus.external.historyTableName=bus_ext_events_history
+org.killbill.persistent.bus.external.sleep=100
+org.killbill.persistent.bus.external.nbThreads=1
+org.killbill.persistent.bus.external.claimed=1
+org.killbill.persistent.bus.external.tableName=bus_ext_events
+org.killbill.persistent.bus.external.historyTableName=bus_ext_events_history
diff --git a/server/src/main/java/org/killbill/billing/server/config/DaoConfig.java b/server/src/main/java/org/killbill/billing/server/config/DaoConfig.java
new file mode 100644
index 0000000..8198f9e
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/config/DaoConfig.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.config;
+
+import org.killbill.billing.server.modules.DataSourceConnectionPoolingType;
+import org.killbill.billing.util.config.KillbillConfig;
+import org.killbill.commons.jdbi.log.LogLevel;
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+import org.skife.config.TimeSpan;
+
+public interface DaoConfig extends KillbillConfig {
+
+ @Description("The jdbc url for the database")
+ @Config("org.killbill.dao.url")
+ @Default("jdbc:mysql://127.0.0.1:3306/killbill")
+ String getJdbcUrl();
+
+ @Description("The jdbc user name for the database")
+ @Config("org.killbill.dao.user")
+ @Default("killbill")
+ String getUsername();
+
+ @Description("The jdbc password for the database")
+ @Config("org.killbill.dao.password")
+ @Default("killbill")
+ String getPassword();
+
+ @Description("The minimum allowed number of idle connections to the database")
+ @Config("org.killbill.dao.minIdle")
+ @Default("1")
+ int getMinIdle();
+
+ @Description("The maximum allowed number of active connections to the database")
+ @Config("org.killbill.dao.maxActive")
+ @Default("30")
+ int getMaxActive();
+
+ @Description("How long to wait before a connection attempt to the database is considered timed out")
+ @Config("org.killbill.dao.connectionTimeout")
+ @Default("10s")
+ TimeSpan getConnectionTimeout();
+
+ @Description("The time for a connection to remain unused before it is closed off")
+ @Config("org.killbill.dao.idleMaxAge")
+ @Default("60m")
+ TimeSpan getIdleMaxAge();
+
+ @Description("Any connections older than this setting will be closed off whether it is idle or not. Connections " +
+ "currently in use will not be affected until they are returned to the pool")
+ @Config("org.killbill.dao.maxConnectionAge")
+ @Default("0m")
+ TimeSpan getMaxConnectionAge();
+
+ @Description("Time for a connection to remain idle before sending a test query to the DB")
+ @Config("org.killbill.dao.idleConnectionTestPeriod")
+ @Default("5m")
+ TimeSpan getIdleConnectionTestPeriod();
+
+ @Description("Log level for SQL queries")
+ @Config("org.killbill.dao.logLevel")
+ @Default("WARN")
+ LogLevel getLogLevel();
+
+ @Description("The TransactionHandler to use for all Handle instances")
+ @Config("org.killbill.dao.transactionHandler")
+ @Default("org.killbill.commons.jdbi.transaction.RestartTransactionRunner")
+ String getTransactionHandlerClass();
+
+ @Description("Connection pooling type")
+ @Config("org.killbill.dao.poolingType")
+ @Default("C3P0")
+ DataSourceConnectionPoolingType getConnectionPoolingType();
+}
diff --git a/server/src/main/java/org/killbill/billing/server/config/KillbillServerConfig.java b/server/src/main/java/org/killbill/billing/server/config/KillbillServerConfig.java
new file mode 100644
index 0000000..883984d
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/config/KillbillServerConfig.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+import org.killbill.billing.util.config.KillbillConfig;
+
+public interface KillbillServerConfig extends KillbillConfig {
+
+ @Config("org.killbill.server.multitenant")
+ @Default("true")
+ @Description("Whether multi-tenancy is enabled")
+ public boolean isMultiTenancyEnabled();
+
+ @Config("org.killbill.server.test.mode")
+ @Default("false")
+ @Description("Whether to start in test mode")
+ public boolean isTestModeEnabled();
+}
diff --git a/server/src/main/java/org/killbill/billing/server/config/UpdateCheckConfig.java b/server/src/main/java/org/killbill/billing/server/config/UpdateCheckConfig.java
new file mode 100644
index 0000000..1778ba7
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/config/UpdateCheckConfig.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.config;
+
+import java.net.URI;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+import org.killbill.billing.util.config.KillbillConfig;
+
+public interface UpdateCheckConfig extends KillbillConfig {
+
+ @Config("org.killbill.server.updateCheck.skip")
+ @Default("false")
+ @Description("Whether to skip update checks")
+ public boolean shouldSkipUpdateCheck();
+
+ @Config("org.killbill.server.updateCheck.url")
+ @Default("https://raw.github.com/killbill/killbill/master/server/src/main/resources/update-checker/killbill-server-update-list.properties")
+ @Description("URL to retrieve the latest version of Kill Bill")
+ public URI updateCheckURL();
+
+ @Config("org.killbill.server.updateCheck.connectTimeout")
+ @Default("3000")
+ @Description("Update check connection timeout")
+ public int updateCheckConnectionTimeout();
+}
diff --git a/server/src/main/java/org/killbill/billing/server/dao/EmbeddedDBFactory.java b/server/src/main/java/org/killbill/billing/server/dao/EmbeddedDBFactory.java
new file mode 100644
index 0000000..475800c
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/dao/EmbeddedDBFactory.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.dao;
+
+import java.net.URI;
+
+import org.killbill.billing.server.config.DaoConfig;
+import org.killbill.commons.embeddeddb.EmbeddedDB;
+import org.killbill.commons.embeddeddb.GenericStandaloneDB;
+import org.killbill.commons.embeddeddb.h2.H2EmbeddedDB;
+import org.killbill.commons.embeddeddb.mysql.MySQLStandaloneDB;
+
+public class EmbeddedDBFactory {
+
+ private EmbeddedDBFactory() { }
+
+ public static EmbeddedDB get(final DaoConfig config) {
+ final URI uri = URI.create(config.getJdbcUrl().substring(5));
+
+ final String databaseName;
+ final String schemeLocation;
+ if (uri.getPath() != null) {
+ schemeLocation = null;
+ databaseName = uri.getPath().split("/")[1].split(";")[0];
+ } else if (uri.getSchemeSpecificPart() != null) {
+ final String[] schemeParts = uri.getSchemeSpecificPart().split(":");
+ schemeLocation = schemeParts[0];
+ databaseName = schemeParts[1].split(";")[0];
+ } else {
+ schemeLocation = null;
+ databaseName = null;
+ }
+
+ if ("mysql".equals(uri.getScheme())) {
+ return new MySQLStandaloneDB(databaseName, config.getUsername(), config.getPassword(), config.getJdbcUrl());
+ } else if ("h2".equals(uri.getScheme()) && ("mem".equals(schemeLocation) || "file".equals(schemeLocation))) {
+ return new H2EmbeddedDB(databaseName, config.getUsername(), config.getPassword(), config.getJdbcUrl());
+ } else {
+ return new GenericStandaloneDB(databaseName, config.getUsername(), config.getPassword(), config.getJdbcUrl());
+ }
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/DefaultServerService.java b/server/src/main/java/org/killbill/billing/server/DefaultServerService.java
new file mode 100644
index 0000000..d2ab3f2
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/DefaultServerService.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.beatrix.glue.BeatrixModule;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.bus.api.PersistentBus.EventBusException;
+import org.killbill.billing.lifecycle.LifecycleHandlerType;
+import org.killbill.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+import org.killbill.billing.server.notifications.PushNotificationListener;
+
+public class DefaultServerService implements ServerService {
+
+ private final static Logger log = LoggerFactory.getLogger(DefaultServerService.class);
+
+ private final static String SERVER_SERVICE = "server-service";
+
+
+ private final PersistentBus bus;
+ private final PushNotificationListener pushNotificationListener;
+
+ @Inject
+ public DefaultServerService(@Named(BeatrixModule.EXTERNAL_BUS) final PersistentBus bus, final PushNotificationListener pushNotificationListener) {
+ this.bus = bus;
+ this.pushNotificationListener = pushNotificationListener;
+ }
+
+ @Override
+ public String getName() {
+ return SERVER_SERVICE;
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.INIT_SERVICE)
+ public void registerForNotifications() {
+ try {
+ bus.register(pushNotificationListener);
+ } catch (EventBusException e) {
+ log.warn("Failed to initialize Server service :", e);
+ }
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE)
+ public void unregisterForNotifications() {
+ try {
+ bus.unregister(pushNotificationListener);
+ } catch (EventBusException e) {
+ log.warn("Failed to stop Server service :", e);
+ }
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/filters/KillbillGuiceFilter.java b/server/src/main/java/org/killbill/billing/server/filters/KillbillGuiceFilter.java
new file mode 100644
index 0000000..b137ff6
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/filters/KillbillGuiceFilter.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.filters;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.server.updatechecker.UpdateChecker;
+
+import com.google.inject.servlet.GuiceFilter;
+
+public class KillbillGuiceFilter extends GuiceFilter {
+
+ private static final Logger log = LoggerFactory.getLogger(KillbillGuiceFilter.class);
+
+ private final UpdateChecker checker = new UpdateChecker();
+
+ @Override
+ public void init(final FilterConfig filterConfig) throws ServletException {
+ super.init(filterConfig);
+
+ // At this point, Kill Bill server is fully initialized
+ log.info("Kill Bill server has started");
+
+ checker.check(filterConfig.getServletContext());
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/healthchecks/KillbillHealthcheck.java b/server/src/main/java/org/killbill/billing/server/healthchecks/KillbillHealthcheck.java
new file mode 100644
index 0000000..5bcdf87
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/healthchecks/KillbillHealthcheck.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.healthchecks;
+
+import org.weakref.jmx.Managed;
+
+import com.codahale.metrics.health.HealthCheck;
+
+public class KillbillHealthcheck extends HealthCheck {
+
+ @Override
+ public Result check() {
+ try {
+ // STEPH obviously needs more than that
+ return Result.healthy();
+ } catch (final Exception e) {
+ return Result.unhealthy(e);
+ }
+ }
+
+ @Managed(description = "Basic killbill healthcheck")
+ public boolean isHealthy() {
+ return check().isHealthy();
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java b/server/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java
new file mode 100644
index 0000000..853185d
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/listeners/KillbillGuiceListener.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.listeners;
+
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+
+import javax.management.MBeanServer;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+
+import org.killbill.billing.beatrix.lifecycle.DefaultLifecycle;
+import org.killbill.billing.jaxrs.resources.JaxRsResourceBase;
+import org.killbill.billing.jaxrs.util.KillbillEventHandler;
+import org.killbill.billing.server.config.DaoConfig;
+import org.killbill.billing.server.config.KillbillServerConfig;
+import org.killbill.billing.server.healthchecks.KillbillHealthcheck;
+import org.killbill.billing.server.modules.KillbillServerModule;
+import org.killbill.billing.server.security.TenantFilter;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.commons.embeddeddb.EmbeddedDB;
+import org.killbill.commons.skeleton.listeners.GuiceServletContextListener;
+import org.killbill.commons.skeleton.modules.BaseServerModuleBuilder;
+import org.killbill.commons.skeleton.modules.ConfigModule;
+import org.killbill.commons.skeleton.modules.JMXModule;
+import org.killbill.commons.skeleton.modules.JaxrsJacksonModule;
+import org.killbill.commons.skeleton.modules.StatsModule;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.skife.config.ConfigurationObjectFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.health.HealthCheckRegistry;
+import com.codahale.metrics.servlets.HealthCheckServlet;
+import com.codahale.metrics.servlets.MetricsServlet;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.management.ManagementService;
+
+public class KillbillGuiceListener extends GuiceServletContextListener {
+
+ public static final Logger logger = LoggerFactory.getLogger(KillbillGuiceListener.class);
+
+ private KillbillServerConfig config;
+ private DaoConfig daoConfig;
+ private Injector injector;
+ private DefaultLifecycle killbillLifecycle;
+ private BusService killbillBusService;
+ private KillbillEventHandler killbilleventHandler;
+ private EmbeddedDB embeddedDB;
+
+ protected Module getModule(final ServletContext servletContext) {
+ return new KillbillServerModule(servletContext, daoConfig, config.isTestModeEnabled());
+ }
+
+ private void registerMBeansForCache(final CacheManager cacheManager) {
+ if (cacheManager != null) {
+ final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
+ ManagementService.registerMBeans(cacheManager, mBeanServer, false, true, true, true);
+ }
+ }
+
+ @Override
+ public void contextInitialized(final ServletContextEvent event) {
+ config = new ConfigurationObjectFactory(System.getProperties()).build(KillbillServerConfig.class);
+ daoConfig = new ConfigurationObjectFactory(System.getProperties()).build(DaoConfig.class);
+
+ // Don't filter all requests through Jersey, only the JAX-RS APIs (otherwise,
+ // things like static resources, favicon, etc. are 404'ed)
+ final BaseServerModuleBuilder builder = new BaseServerModuleBuilder().setJaxrsUriPattern("(" + JaxRsResourceBase.PREFIX + "|" + JaxRsResourceBase.PLUGINS_PATH + ")" + "/.*")
+ .addJaxrsResource("org.killbill.billing.jaxrs.mappers")
+ .addJaxrsResource("org.killbill.billing.jaxrs.resources");
+
+ if (config.isMultiTenancyEnabled()) {
+ builder.addFilter("/*", TenantFilter.class);
+ }
+
+ guiceModules = ImmutableList.<Module>of(builder.build(),
+ new ConfigModule(KillbillServerConfig.class, DaoConfig.class),
+ new JaxrsJacksonModule(new ObjectMapper()),
+ new JMXModule(KillbillHealthcheck.class, NotificationQueueService.class, PersistentBus.class),
+ new StatsModule(KillbillHealthcheck.class),
+ getModule(event.getServletContext()));
+
+ super.contextInitialized(event);
+
+ logger.info("KillbillLifecycleListener : contextInitialized");
+
+ injector = injector(event);
+ event.getServletContext().setAttribute(Injector.class.getName(), injector);
+
+ // Metrics initialization
+ event.getServletContext().setAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY, injector.getInstance(HealthCheckRegistry.class));
+ event.getServletContext().setAttribute(MetricsServlet.METRICS_REGISTRY, injector.getInstance(MetricRegistry.class));
+
+ killbillLifecycle = injector.getInstance(DefaultLifecycle.class);
+ killbillBusService = injector.getInstance(BusService.class);
+ killbilleventHandler = injector.getInstance(KillbillEventHandler.class);
+ // Already started at this point
+ embeddedDB = injector.getInstance(EmbeddedDB.class);
+
+ registerMBeansForCache(injector.getInstance(CacheManager.class));
+
+ //
+ // Fire all Startup levels up to service start
+ //
+ killbillLifecycle.fireStartupSequencePriorEventRegistration();
+ //
+ // Perform Bus registration
+ //
+ try {
+ killbillBusService.getBus().register(killbilleventHandler);
+ } catch (PersistentBus.EventBusException e) {
+ logger.error("Failed to register for event notifications, this is bad exiting!", e);
+ System.exit(1);
+ }
+ // Let's start!
+ killbillLifecycle.fireStartupSequencePostEventRegistration();
+ }
+
+ @Override
+ public void contextDestroyed(final ServletContextEvent sce) {
+ super.contextDestroyed(sce);
+
+ logger.info("IrsKillbillListener : contextDestroyed");
+ // Stop services
+ // Guice error, no need to fill the screen with useless stack traces
+ if (killbillLifecycle == null) {
+ return;
+ }
+
+ killbillLifecycle.fireShutdownSequencePriorEventUnRegistration();
+
+ try {
+ killbillBusService.getBus().unregister(killbilleventHandler);
+ } catch (PersistentBus.EventBusException e) {
+ logger.warn("Failed to unregister for event notifications", e);
+ }
+
+ // Complete shutdown sequence
+ killbillLifecycle.fireShutdownSequencePostEventUnRegistration();
+
+ if (embeddedDB != null) {
+ try {
+ embeddedDB.stop();
+ } catch (final IOException ignored) {
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public Injector getInstantiatedInjector() {
+ return injector;
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/modules/DataSourceConnectionPoolingType.java b/server/src/main/java/org/killbill/billing/server/modules/DataSourceConnectionPoolingType.java
new file mode 100644
index 0000000..c152448
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/modules/DataSourceConnectionPoolingType.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.modules;
+
+public enum DataSourceConnectionPoolingType {
+ C3P0,
+ BONECP
+}
diff --git a/server/src/main/java/org/killbill/billing/server/modules/DataSourceProvider.java b/server/src/main/java/org/killbill/billing/server/modules/DataSourceProvider.java
new file mode 100644
index 0000000..9f4d3ad
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/modules/DataSourceProvider.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.modules;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.sql.DataSource;
+
+import org.killbill.billing.server.config.DaoConfig;
+import org.skife.config.TimeSpan;
+
+import com.jolbox.bonecp.BoneCPConfig;
+import com.jolbox.bonecp.BoneCPDataSource;
+import com.mchange.v2.c3p0.ComboPooledDataSource;
+
+public class DataSourceProvider implements Provider<DataSource> {
+
+ private final DaoConfig config;
+
+ @Inject
+ public DataSourceProvider(final DaoConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public DataSource get() {
+ return getDataSource();
+ }
+
+ private DataSource getDataSource() {
+ final DataSource ds;
+
+ if (DataSourceConnectionPoolingType.C3P0.equals(config.getConnectionPoolingType())) {
+ ds = getC3P0DataSource();
+ } else if (DataSourceConnectionPoolingType.BONECP.equals(config.getConnectionPoolingType())) {
+ ds = getBoneCPDatSource();
+ } else {
+ throw new IllegalArgumentException("DataSource " + config.getConnectionPoolingType() + " unsupported");
+ }
+
+ return ds;
+ }
+
+ private DataSource getBoneCPDatSource() {
+ final BoneCPConfig dbConfig = new BoneCPConfig();
+ dbConfig.setJdbcUrl(config.getJdbcUrl());
+ dbConfig.setUsername(config.getUsername());
+ dbConfig.setPassword(config.getPassword());
+ dbConfig.setMinConnectionsPerPartition(config.getMinIdle());
+ dbConfig.setMaxConnectionsPerPartition(config.getMaxActive());
+ dbConfig.setConnectionTimeout(config.getConnectionTimeout().getPeriod(), config.getConnectionTimeout().getUnit());
+ dbConfig.setIdleMaxAge(config.getIdleMaxAge().getPeriod(), config.getIdleMaxAge().getUnit());
+ dbConfig.setMaxConnectionAge(config.getMaxConnectionAge().getPeriod(), config.getMaxConnectionAge().getUnit());
+ dbConfig.setIdleConnectionTestPeriod(config.getIdleConnectionTestPeriod().getPeriod(), config.getIdleConnectionTestPeriod().getUnit());
+ dbConfig.setPartitionCount(1);
+ dbConfig.setDisableJMX(false);
+
+ return new BoneCPDataSource(dbConfig);
+ }
+
+ private DataSource getC3P0DataSource() {
+ final ComboPooledDataSource cpds = new ComboPooledDataSource();
+ cpds.setJdbcUrl(config.getJdbcUrl());
+ cpds.setUser(config.getUsername());
+ cpds.setPassword(config.getPassword());
+ // http://www.mchange.com/projects/c3p0/#minPoolSize
+ // Minimum number of Connections a pool will maintain at any given time.
+ cpds.setMinPoolSize(config.getMinIdle());
+ // http://www.mchange.com/projects/c3p0/#maxPoolSize
+ // Maximum number of Connections a pool will maintain at any given time.
+ cpds.setMaxPoolSize(config.getMaxActive());
+ // http://www.mchange.com/projects/c3p0/#checkoutTimeout
+ // The number of milliseconds a client calling getConnection() will wait for a Connection to be checked-in or
+ // acquired when the pool is exhausted. Zero means wait indefinitely. Setting any positive value will cause the getConnection()
+ // call to time-out and break with an SQLException after the specified number of milliseconds.
+ cpds.setCheckoutTimeout(toMilliSeconds(config.getConnectionTimeout()));
+ // http://www.mchange.com/projects/c3p0/#maxIdleTime
+ // Seconds a Connection can remain pooled but unused before being discarded. Zero means idle connections never expire.
+ cpds.setMaxIdleTime(toSeconds(config.getIdleMaxAge()));
+ // http://www.mchange.com/projects/c3p0/#maxConnectionAge
+ // Seconds, effectively a time to live. A Connection older than maxConnectionAge will be destroyed and purged from the pool.
+ // This differs from maxIdleTime in that it refers to absolute age. Even a Connection which has not been much idle will be purged
+ // from the pool if it exceeds maxConnectionAge. Zero means no maximum absolute age is enforced.
+ cpds.setMaxConnectionAge(toSeconds(config.getMaxConnectionAge()));
+ // http://www.mchange.com/projects/c3p0/#idleConnectionTestPeriod
+ // If this is a number greater than 0, c3p0 will test all idle, pooled but unchecked-out connections, every this number of seconds.
+ cpds.setIdleConnectionTestPeriod(toSeconds(config.getIdleConnectionTestPeriod()));
+
+ return cpds;
+ }
+
+ private int toSeconds(final TimeSpan timeSpan) {
+ return toSeconds(timeSpan.getPeriod(), timeSpan.getUnit());
+ }
+
+ private int toSeconds(final long period, final TimeUnit timeUnit) {
+ return (int) TimeUnit.SECONDS.convert(period, timeUnit);
+ }
+
+ private int toMilliSeconds(final TimeSpan timeSpan) {
+ return toMilliSeconds(timeSpan.getPeriod(), timeSpan.getUnit());
+ }
+
+ private int toMilliSeconds(final long period, final TimeUnit timeUnit) {
+ return (int) TimeUnit.MILLISECONDS.convert(period, timeUnit);
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/modules/DBIProvider.java b/server/src/main/java/org/killbill/billing/server/modules/DBIProvider.java
new file mode 100644
index 0000000..96a81df
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/modules/DBIProvider.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.modules;
+
+import javax.sql.DataSource;
+
+import org.killbill.billing.server.config.DaoConfig;
+import org.killbill.billing.util.dao.AuditLogModelDaoMapper;
+import org.killbill.billing.util.dao.DateTimeArgumentFactory;
+import org.killbill.billing.util.dao.DateTimeZoneArgumentFactory;
+import org.killbill.billing.util.dao.EnumArgumentFactory;
+import org.killbill.billing.util.dao.LocalDateArgumentFactory;
+import org.killbill.billing.util.dao.RecordIdIdMappingsMapper;
+import org.killbill.billing.util.dao.UUIDArgumentFactory;
+import org.killbill.billing.util.dao.UuidMapper;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.TimingCollector;
+import org.skife.jdbi.v2.tweak.SQLLog;
+import org.skife.jdbi.v2.tweak.TransactionHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.jdbi.InstrumentedTimingCollector;
+import com.codahale.metrics.jdbi.strategies.BasicSqlNameStrategy;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class DBIProvider implements Provider<DBI> {
+
+ private static final Logger logger = LoggerFactory.getLogger(DBIProvider.class);
+
+ private final DataSource ds;
+ private final MetricRegistry metricsRegistry;
+ private final DaoConfig config;
+ private SQLLog sqlLog;
+
+ @Inject
+ public DBIProvider(final DataSource ds, final MetricRegistry metricsRegistry, final DaoConfig config) {
+ this.ds = ds;
+ this.metricsRegistry = metricsRegistry;
+ this.config = config;
+ }
+
+ @Inject(optional = true)
+ public void setSqlLog(final SQLLog sqlLog) {
+ this.sqlLog = sqlLog;
+ }
+
+ @Override
+ public DBI get() {
+ final DBI dbi = new DBI(ds);
+ dbi.registerArgumentFactory(new UUIDArgumentFactory());
+ dbi.registerArgumentFactory(new DateTimeZoneArgumentFactory());
+ dbi.registerArgumentFactory(new DateTimeArgumentFactory());
+ dbi.registerArgumentFactory(new LocalDateArgumentFactory());
+ dbi.registerArgumentFactory(new EnumArgumentFactory());
+ dbi.registerMapper(new UuidMapper());
+ dbi.registerMapper(new AuditLogModelDaoMapper());
+ dbi.registerMapper(new RecordIdIdMappingsMapper());
+
+ if (sqlLog != null) {
+ dbi.setSQLLog(sqlLog);
+ }
+
+ if (config.getTransactionHandlerClass() != null) {
+ logger.info("Using " + config.getTransactionHandlerClass() + " as a transaction handler class");
+ try {
+ dbi.setTransactionHandler((TransactionHandler) Class.forName(config.getTransactionHandlerClass()).newInstance());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ final BasicSqlNameStrategy basicSqlNameStrategy = new BasicSqlNameStrategy();
+ final TimingCollector timingCollector = new InstrumentedTimingCollector(metricsRegistry, basicSqlNameStrategy);
+ dbi.setTimingCollector(timingCollector);
+
+ return dbi;
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/modules/EmbeddedDBProvider.java b/server/src/main/java/org/killbill/billing/server/modules/EmbeddedDBProvider.java
new file mode 100644
index 0000000..bae328c
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/modules/EmbeddedDBProvider.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.modules;
+
+import java.io.IOException;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.server.config.DaoConfig;
+import org.killbill.billing.server.dao.EmbeddedDBFactory;
+import org.killbill.billing.util.io.IOUtils;
+import org.killbill.commons.embeddeddb.EmbeddedDB;
+import org.killbill.commons.embeddeddb.EmbeddedDB.DBEngine;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.io.Resources;
+import com.google.inject.Provider;
+
+public class EmbeddedDBProvider implements Provider<EmbeddedDB> {
+
+ private static final Logger logger = LoggerFactory.getLogger(EmbeddedDBProvider.class);
+
+ private final DaoConfig config;
+
+ @Inject
+ public EmbeddedDBProvider(final DaoConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public EmbeddedDB get() {
+ final EmbeddedDB embeddedDB = EmbeddedDBFactory.get(config);
+
+ if (DBEngine.H2.equals(embeddedDB.getDBEngine())) {
+ try {
+ // Standalone mode?
+ initializeEmbeddedDB(embeddedDB);
+ } catch (final IOException e) {
+ logger.error("Error while initializing H2, opportunistically continuing the startup sequence", e);
+ }
+ }
+
+ return embeddedDB;
+ }
+
+ private void initializeEmbeddedDB(final EmbeddedDB embeddedDB) throws IOException {
+ embeddedDB.initialize();
+ embeddedDB.start();
+
+ // If the tables have not been created yet, do it, otherwise don't clobber them
+ if (!embeddedDB.getAllTables().isEmpty()) {
+ return;
+ }
+
+ for (final String module : new String[]{"account",
+ "beatrix",
+ "entitlement",
+ "invoice",
+ "payment",
+ "subscription",
+ "tenant",
+ "usage",
+ "util"}) {
+ final String ddl = IOUtils.toString(Resources.getResource("org/killbill/billing/" + module + "/ddl.sql").openStream());
+ embeddedDB.executeScript(ddl);
+ }
+ embeddedDB.refreshTableNames();
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java b/server/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java
new file mode 100644
index 0000000..d145ce6
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/modules/KillbillServerModule.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.modules;
+
+import javax.servlet.ServletContext;
+import javax.sql.DataSource;
+
+import org.killbill.billing.account.glue.DefaultAccountModule;
+import org.killbill.billing.beatrix.glue.BeatrixModule;
+import org.killbill.billing.catalog.glue.CatalogModule;
+import org.killbill.billing.currency.glue.CurrencyModule;
+import org.killbill.billing.entitlement.glue.DefaultEntitlementModule;
+import org.killbill.billing.invoice.glue.DefaultInvoiceModule;
+import org.killbill.billing.jaxrs.resources.AccountResource;
+import org.killbill.billing.jaxrs.resources.BundleResource;
+import org.killbill.billing.jaxrs.resources.CatalogResource;
+import org.killbill.billing.jaxrs.resources.CustomFieldResource;
+import org.killbill.billing.jaxrs.resources.ExportResource;
+import org.killbill.billing.jaxrs.resources.InvoiceResource;
+import org.killbill.billing.jaxrs.resources.PaymentMethodResource;
+import org.killbill.billing.jaxrs.resources.PaymentResource;
+import org.killbill.billing.jaxrs.resources.PluginResource;
+import org.killbill.billing.jaxrs.resources.RefundResource;
+import org.killbill.billing.jaxrs.resources.SubscriptionResource;
+import org.killbill.billing.jaxrs.resources.TagDefinitionResource;
+import org.killbill.billing.jaxrs.resources.TagResource;
+import org.killbill.billing.jaxrs.resources.TenantResource;
+import org.killbill.billing.jaxrs.resources.TestResource;
+import org.killbill.billing.jaxrs.util.KillbillEventHandler;
+import org.killbill.billing.junction.glue.DefaultJunctionModule;
+import org.killbill.billing.osgi.glue.DefaultOSGIModule;
+import org.killbill.billing.overdue.glue.DefaultOverdueModule;
+import org.killbill.billing.payment.glue.PaymentModule;
+import org.killbill.billing.server.DefaultServerService;
+import org.killbill.billing.server.ServerService;
+import org.killbill.billing.server.config.DaoConfig;
+import org.killbill.billing.server.notifications.PushNotificationListener;
+import org.killbill.billing.subscription.glue.DefaultSubscriptionModule;
+import org.killbill.billing.tenant.glue.TenantModule;
+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.BusModule;
+import org.killbill.billing.util.glue.CacheModule;
+import org.killbill.billing.util.glue.CallContextModule;
+import org.killbill.billing.util.glue.ClockModule;
+import org.killbill.billing.util.glue.CustomFieldModule;
+import org.killbill.billing.util.glue.ExportModule;
+import org.killbill.billing.util.glue.GlobalLockerModule;
+import org.killbill.billing.util.glue.KillBillShiroAopModule;
+import org.killbill.billing.util.glue.NonEntityDaoModule;
+import org.killbill.billing.util.glue.NotificationQueueModule;
+import org.killbill.billing.util.glue.RecordIdModule;
+import org.killbill.billing.util.glue.SecurityModule;
+import org.killbill.billing.util.glue.TagStoreModule;
+import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
+import org.killbill.commons.embeddeddb.EmbeddedDB;
+import org.skife.config.ConfigSource;
+import org.skife.config.SimplePropertyConfigSource;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.IDBI;
+
+import com.google.inject.AbstractModule;
+
+public class KillbillServerModule extends AbstractModule {
+
+ protected final ServletContext servletContext;
+ private final DaoConfig daoConfig;
+ private final boolean isTestModeEnabled;
+
+ public KillbillServerModule(final ServletContext servletContext, final DaoConfig daoConfig, final boolean testModeEnabled) {
+ this.servletContext = servletContext;
+ this.daoConfig = daoConfig;
+ this.isTestModeEnabled = testModeEnabled;
+ }
+
+ @Override
+ protected void configure() {
+ configureDao();
+ configureResources();
+ installKillbillModules();
+ configurePushNotification();
+ }
+
+ protected void configurePushNotification() {
+ bind(ServerService.class).to(DefaultServerService.class).asEagerSingleton();
+ bind(PushNotificationListener.class).asEagerSingleton();
+ }
+
+ protected void configureDao() {
+ // Load mysql driver if needed
+ try {
+ Class.forName("com.mysql.jdbc.Driver").newInstance();
+ } catch (final Exception ignore) {
+ }
+ bind(IDBI.class).to(DBI.class).asEagerSingleton();
+ bind(DataSource.class).toProvider(DataSourceProvider.class).asEagerSingleton();
+ bind(DBI.class).toProvider(DBIProvider.class).asEagerSingleton();
+ }
+
+ protected void configureResources() {
+ bind(AccountResource.class).asEagerSingleton();
+ bind(BundleResource.class).asEagerSingleton();
+ bind(SubscriptionResource.class).asEagerSingleton();
+ bind(InvoiceResource.class).asEagerSingleton();
+ bind(CustomFieldResource.class).asEagerSingleton();
+ bind(TagResource.class).asEagerSingleton();
+ bind(TagDefinitionResource.class).asEagerSingleton();
+ bind(CatalogResource.class).asEagerSingleton();
+ bind(PaymentMethodResource.class).asEagerSingleton();
+ bind(PaymentResource.class).asEagerSingleton();
+ bind(PluginResource.class).asEagerSingleton();
+ bind(RefundResource.class).asEagerSingleton();
+ bind(TenantResource.class).asEagerSingleton();
+ bind(ExportResource.class).asEagerSingleton();
+ bind(PluginResource.class).asEagerSingleton();
+ bind(TenantResource.class).asEagerSingleton();
+ bind(KillbillEventHandler.class).asEagerSingleton();
+ }
+
+ protected void installClock() {
+ if (isTestModeEnabled) {
+ bind(Clock.class).to(ClockMock.class).asEagerSingleton();
+ bind(TestResource.class).asEagerSingleton();
+ } else {
+ install(new ClockModule());
+ }
+ }
+
+ protected void installKillbillModules() {
+ final ConfigSource configSource = new SimplePropertyConfigSource(System.getProperties());
+
+ // TODO Pierre Refactor GlobalLockerModule for this to be a real provider?
+ final EmbeddedDBProvider embeddedDBProvider = new EmbeddedDBProvider(daoConfig);
+ final EmbeddedDB embeddedDB = embeddedDBProvider.get();
+ bind(EmbeddedDB.class).toInstance(embeddedDB);
+
+ install(new EmailModule(configSource));
+ install(new CacheModule(configSource));
+ install(new GlobalLockerModule(embeddedDB.getDBEngine()));
+ install(new CustomFieldModule());
+ install(new AuditModule());
+ install(new CatalogModule(configSource));
+ install(new BusModule(configSource));
+ install(new NotificationQueueModule(configSource));
+ install(new CallContextModule());
+ install(new DefaultAccountModule(configSource));
+ install(new DefaultInvoiceModule(configSource));
+ install(new TemplateModule());
+ install(new DefaultSubscriptionModule(configSource));
+ install(new DefaultEntitlementModule(configSource));
+ install(new PaymentModule(configSource));
+ install(new BeatrixModule(configSource));
+ install(new DefaultJunctionModule(configSource));
+ install(new DefaultOverdueModule(configSource));
+ install(new CurrencyModule(configSource));
+ install(new TenantModule(configSource));
+ install(new ExportModule());
+ install(new TagStoreModule());
+ install(new NonEntityDaoModule());
+ install(new DefaultOSGIModule(configSource));
+ install(new UsageModule(configSource));
+ install(new RecordIdModule());
+ install(new KillBillShiroWebModule(servletContext, configSource));
+ install(new KillBillShiroAopModule());
+ install(new SecurityModule());
+ installClock();
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java b/server/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
new file mode 100644
index 0000000..e8c9440
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.modules;
+
+import javax.servlet.ServletContext;
+
+import org.apache.shiro.cache.CacheManager;
+import org.apache.shiro.guice.web.ShiroWebModule;
+import org.apache.shiro.session.mgt.SessionManager;
+import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.jaxrs.resources.JaxrsResource;
+import org.killbill.billing.util.config.RbacConfig;
+import org.killbill.billing.util.glue.EhCacheManagerProvider;
+import org.killbill.billing.util.glue.IniRealmProvider;
+import org.killbill.billing.util.glue.JDBCSessionDaoProvider;
+import org.killbill.billing.util.glue.KillBillShiroModule;
+import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
+import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
+
+import com.google.inject.binder.AnnotatedBindingBuilder;
+
+// For Kill Bill server only.
+// See org.killbill.billing.util.glue.KillBillShiroModule for Kill Bill library.
+public class KillBillShiroWebModule extends ShiroWebModule {
+
+ private final ConfigSource configSource;
+
+ public KillBillShiroWebModule(final ServletContext servletContext, final ConfigSource configSource) {
+ super(servletContext);
+ this.configSource = configSource;
+ }
+
+ @Override
+ protected void configureShiroWeb() {
+ final RbacConfig config = new ConfigurationObjectFactory(configSource).build(RbacConfig.class);
+ bind(RbacConfig.class).toInstance(config);
+
+ bindRealm().toProvider(IniRealmProvider.class).asEagerSingleton();
+
+ if (KillBillShiroModule.isLDAPEnabled()) {
+ bindRealm().to(KillBillJndiLdapRealm.class).asEagerSingleton();
+ }
+
+ // Magic provider to configure the cache manager
+ bind(CacheManager.class).toProvider(EhCacheManagerProvider.class).asEagerSingleton();
+
+ if (KillBillShiroModule.isRBACEnabled()) {
+ addFilterChain(JaxrsResource.PREFIX + "/**", AUTHC_BASIC);
+ }
+ }
+
+ @Override
+ protected void bindSessionManager(final AnnotatedBindingBuilder<SessionManager> bind) {
+ // Bypass the servlet container completely for session management and delegate it to Shiro.
+ // The default session timeout is 30 minutes.
+ bind.to(DefaultWebSessionManager.class).asEagerSingleton();
+
+ // Magic provider to configure the session DAO
+ bind(JDBCSessionDao.class).toProvider(JDBCSessionDaoProvider.class).asEagerSingleton();
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/notifications/PushNotificationListener.java b/server/src/main/java/org/killbill/billing/server/notifications/PushNotificationListener.java
new file mode 100644
index 0000000..319e821
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/notifications/PushNotificationListener.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.notifications;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.jaxrs.json.NotificationJson;
+import org.killbill.billing.notification.plugin.api.ExtBusEvent;
+import org.killbill.billing.tenant.api.TenantApiException;
+import org.killbill.billing.tenant.api.TenantKV.TenantKey;
+import org.killbill.billing.tenant.api.TenantUserApi;
+import org.killbill.billing.util.callcontext.CallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+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.databind.ObjectMapper;
+import com.google.common.eventbus.Subscribe;
+
+public class PushNotificationListener {
+
+ private final static Logger log = LoggerFactory.getLogger(PushNotificationListener.class);
+
+ private final static int TIMEOUT_NOTIFCATION = 15; // 15 seconds
+
+ private final TenantUserApi tenantApi;
+ private final CallContextFactory contextFactory;
+ private final AsyncHttpClient httpClient;
+ private final ObjectMapper mapper;
+
+
+ @Inject
+ public PushNotificationListener(final ObjectMapper mapper, final TenantUserApi tenantApi, final CallContextFactory contextFactory) {
+ this.httpClient = new AsyncHttpClient(new AsyncHttpClientConfig.Builder().setRequestTimeoutInMs(TIMEOUT_NOTIFCATION * 1000).build());
+ this.tenantApi = tenantApi;
+ this.contextFactory = contextFactory;
+ this.mapper = mapper;
+ }
+
+ @Subscribe
+ public void triggerPushNotifications(final ExtBusEvent event) {
+
+ final TenantContext context = contextFactory.createTenantContext(event.getTenantId());
+ try {
+ final List<String> callbacks = getCallbacksForTenant(context);
+ dispatchCallback(event.getTenantId(), event, callbacks);
+ } catch (final TenantApiException e) {
+ log.warn("Failed to retrieve push notification callback for tenant {}", event.getTenantId());
+ } catch (final IOException e) {
+ log.warn("Failed to retrieve push notification callback for tenant {}", event.getTenantId());
+ }
+ }
+
+ private void dispatchCallback(final UUID tenantId, final ExtBusEvent event, final List<String> callbacks) throws IOException {
+ final NotificationJson notification = new NotificationJson(event);
+ final String body = mapper.writeValueAsString(notification);
+ for (final String cur : callbacks) {
+ doPost(tenantId, cur, body, TIMEOUT_NOTIFCATION);
+ }
+ }
+
+
+ private boolean doPost(final UUID tenantId, final String url, final String body, final int timeoutSec) {
+
+ final BoundRequestBuilder builder = httpClient.preparePost(url);
+ builder.setBody(body == null ? "{}" : body);
+
+ Response response = null;
+ try {
+ final ListenableFuture<Response> futureStatus =
+ builder.execute(new AsyncCompletionHandler<Response>() {
+ @Override
+ public Response onCompleted(final Response response) throws Exception {
+ return response;
+ }
+ });
+ response = futureStatus.get(timeoutSec, TimeUnit.SECONDS);
+ } catch (final Exception e) {
+ log.warn(String.format("Fail to psh notification {} for the tenant {} ", url, tenantId), e);
+ return false;
+ }
+ return response.getStatusCode() >= 200 && response.getStatusCode() < 300;
+ }
+
+ private List<String> getCallbacksForTenant(final TenantContext context) throws TenantApiException {
+ return tenantApi.getTenantValueForKey(TenantKey.PUSH_NOTIFICATION_CB.toString(), context);
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/security/KillbillJdbcRealm.java b/server/src/main/java/org/killbill/billing/server/security/KillbillJdbcRealm.java
new file mode 100644
index 0000000..406f580
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/security/KillbillJdbcRealm.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.security;
+
+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.codec.Base64;
+import org.apache.shiro.realm.jdbc.JdbcRealm;
+import org.apache.shiro.util.ByteSource;
+import org.killbill.billing.server.config.DaoConfig;
+import org.killbill.billing.tenant.security.KillbillCredentialsMatcher;
+import org.skife.config.ConfigurationObjectFactory;
+
+import com.jolbox.bonecp.BoneCPConfig;
+import com.jolbox.bonecp.BoneCPDataSource;
+
+/**
+ * @see {shiro.ini}
+ */
+public class KillbillJdbcRealm extends JdbcRealm {
+
+ private static final String KILLBILL_AUTHENTICATION_QUERY = "select api_secret, api_salt from tenants where api_key = ?";
+
+ public KillbillJdbcRealm() {
+ super();
+ configureSecurity();
+ configureQueries();
+ configureDataSource();
+ }
+
+ @Override
+ protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token) throws AuthenticationException {
+ final SimpleAuthenticationInfo authenticationInfo = (SimpleAuthenticationInfo) super.doGetAuthenticationInfo(token);
+
+ // We store the salt bytes in Base64 (because the JdbcRealm retrieves it as a String)
+ final ByteSource base64Salt = authenticationInfo.getCredentialsSalt();
+ authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(Base64.decode(base64Salt.getBytes())));
+
+ return authenticationInfo;
+ }
+
+ private void configureSecurity() {
+ setSaltStyle(SaltStyle.COLUMN);
+ setCredentialsMatcher(KillbillCredentialsMatcher.getCredentialsMatcher());
+ }
+
+ private void configureQueries() {
+ setAuthenticationQuery(KILLBILL_AUTHENTICATION_QUERY);
+ }
+
+ private void configureDataSource() {
+ // This class is initialized by Shiro, not Guice - we need to retrieve the config manually
+ final DaoConfig config = new ConfigurationObjectFactory(System.getProperties()).build(DaoConfig.class);
+
+ final BoneCPConfig dbConfig = new BoneCPConfig();
+ dbConfig.setJdbcUrl(config.getJdbcUrl());
+ dbConfig.setUsername(config.getUsername());
+ dbConfig.setPassword(config.getPassword());
+ dbConfig.setMinConnectionsPerPartition(config.getMinIdle());
+ dbConfig.setMaxConnectionsPerPartition(config.getMaxActive());
+ dbConfig.setConnectionTimeout(config.getConnectionTimeout().getPeriod(), config.getConnectionTimeout().getUnit());
+ dbConfig.setIdleMaxAge(config.getIdleMaxAge().getPeriod(), config.getIdleMaxAge().getUnit());
+ dbConfig.setMaxConnectionAge(config.getMaxConnectionAge().getPeriod(), config.getMaxConnectionAge().getUnit());
+ dbConfig.setIdleConnectionTestPeriod(config.getIdleConnectionTestPeriod().getPeriod(), config.getIdleConnectionTestPeriod().getUnit());
+ dbConfig.setPartitionCount(1);
+ dbConfig.setDefaultTransactionIsolation("READ_COMMITTED");
+ dbConfig.setDisableJMX(false);
+
+ setDataSource(new BoneCPDataSource(dbConfig));
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/security/TenantFilter.java b/server/src/main/java/org/killbill/billing/server/security/TenantFilter.java
new file mode 100644
index 0000000..ed76073
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/security/TenantFilter.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.security;
+
+import java.io.IOException;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
+import org.apache.shiro.realm.Realm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.jaxrs.resources.JaxrsResource;
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.tenant.api.TenantApiException;
+import org.killbill.billing.tenant.api.TenantUserApi;
+
+import com.google.common.collect.ImmutableList;
+
+@Singleton
+public class TenantFilter implements Filter {
+
+ // See org.killbill.billing.jaxrs.util.Context
+ public static final String TENANT = "killbill_tenant";
+
+ private static final Logger log = LoggerFactory.getLogger(TenantFilter.class);
+
+ @Inject
+ private TenantUserApi tenantUserApi;
+
+ private final ModularRealmAuthenticator modularRealmAuthenticator;
+
+ public TenantFilter() {
+ final Realm killbillJdbcRealm = new KillbillJdbcRealm();
+
+ // We use Shiro to verify the api credentials - but the Shiro Subject is only used for RBAC
+ modularRealmAuthenticator = new ModularRealmAuthenticator();
+ modularRealmAuthenticator.setRealms(ImmutableList.<Realm>of(killbillJdbcRealm));
+ }
+
+ @Override
+ public void init(final FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
+ if (shouldSkipFilter(request)) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ // Lookup tenant information in the headers
+ String apiKey = null;
+ String apiSecret = null;
+ if (request instanceof HttpServletRequest) {
+ final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+ apiKey = httpServletRequest.getHeader(JaxrsResource.HDR_API_KEY);
+ apiSecret = httpServletRequest.getHeader(JaxrsResource.HDR_API_SECRET);
+ }
+
+ // Multi-tenancy is enabled if this filter is installed, we can't continue without credentials
+ if (apiKey == null || apiSecret == null) {
+ final String errorMessage = String.format("Make sure to set the %s and %s headers", JaxrsResource.HDR_API_KEY, JaxrsResource.HDR_API_SECRET);
+ sendAuthError(response, errorMessage);
+ return;
+ }
+
+ // Verify the apiKey/apiSecret combo
+ final AuthenticationToken token = new UsernamePasswordToken(apiKey, apiSecret);
+ try {
+ modularRealmAuthenticator.authenticate(token);
+ } catch (AuthenticationException e) {
+ final String errorMessage = e.getLocalizedMessage();
+ sendAuthError(response, errorMessage);
+ return;
+ }
+
+ try {
+ // Load the tenant in the request object (apiKey is unique across tenants)
+ final Tenant tenant = tenantUserApi.getTenantByApiKey(apiKey);
+ request.setAttribute(TENANT, tenant);
+
+ chain.doFilter(request, response);
+ } catch (TenantApiException e) {
+ // Should never happen since Shiro validated the credentials?
+ log.warn("Couldn't find the tenant?", e);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ private boolean shouldSkipFilter(final ServletRequest request) {
+ boolean shouldSkip = false;
+
+ // Chicken - egg problem
+ if (request instanceof HttpServletRequest) {
+ final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+ final String path = httpServletRequest.getRequestURI();
+ if ("/1.0/kb/tenants".equals(path) && "POST".equals(httpServletRequest.getMethod())) {
+ shouldSkip = true;
+ }
+ }
+
+ return shouldSkip;
+ }
+
+ private void sendAuthError(final ServletResponse response, final String errorMessage) throws IOException {
+ if (response instanceof HttpServletResponse) {
+ final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+ httpServletResponse.sendError(401, errorMessage);
+ }
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/ServerService.java b/server/src/main/java/org/killbill/billing/server/ServerService.java
new file mode 100644
index 0000000..606f549
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/ServerService.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.server;
+
+import org.killbill.billing.lifecycle.KillbillService;
+
+public interface ServerService extends KillbillService {
+
+}
diff --git a/server/src/main/java/org/killbill/billing/server/updatechecker/ClientInfo.java b/server/src/main/java/org/killbill/billing/server/updatechecker/ClientInfo.java
new file mode 100644
index 0000000..5e4ff67
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/updatechecker/ClientInfo.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.updatechecker;
+
+import java.net.InetAddress;
+import java.util.Properties;
+
+import javax.servlet.ServletContext;
+
+import com.google.common.base.StandardSystemProperty;
+import com.google.common.base.Strings;
+
+/**
+ * Gather client-side information
+ * <p/>
+ * We try not to gather any personally identifiable information, only
+ * specifications about the installation (OS, JVM). This helps us
+ * focus our development efforts.
+ */
+public class ClientInfo {
+
+ private static final String UNKNOWN = "UNKNOWN";
+
+ private static int CLIENT_ID;
+
+ static {
+ try {
+ CLIENT_ID = InetAddress.getLocalHost().hashCode();
+ } catch (Throwable t) {
+ CLIENT_ID = 0;
+ }
+ }
+
+ private final ServletContext servletContext;
+ private final Properties props;
+
+ public ClientInfo(final ServletContext servletContext) {
+ this.servletContext = servletContext;
+ this.props = System.getProperties();
+ }
+
+ public String getServletMajorVersion() {
+ return getSanitizedString(String.valueOf(servletContext.getMajorVersion()));
+ }
+
+ public String getServletMinorVersion() {
+ return getSanitizedString(String.valueOf(servletContext.getMinorVersion()));
+ }
+
+ public String getServletEffectiveMajorVersion() {
+ return getSanitizedString(String.valueOf(servletContext.getEffectiveMajorVersion()));
+ }
+
+ public String getServletEffectiveMinorVersion() {
+ return getSanitizedString(String.valueOf(servletContext.getEffectiveMinorVersion()));
+ }
+
+ public String getServerInfo() {
+ return getSanitizedString(servletContext.getServerInfo());
+ }
+
+ public String getClientId() {
+ return String.valueOf(CLIENT_ID);
+ }
+
+ public String getJavaVersion() {
+ return getProperty(StandardSystemProperty.JAVA_VERSION);
+ }
+
+ public String getJavaVendor() {
+ return getProperty(StandardSystemProperty.JAVA_VENDOR);
+ }
+
+ public String getJavaVendorURL() {
+ return getProperty(StandardSystemProperty.JAVA_VENDOR_URL);
+ }
+
+ public String getJavaVMSpecificationVersion() {
+ return getProperty(StandardSystemProperty.JAVA_VM_SPECIFICATION_VERSION);
+ }
+
+ public String getJavaVMSpecificationVendor() {
+ return getProperty(StandardSystemProperty.JAVA_VM_SPECIFICATION_VENDOR);
+ }
+
+ public String getJavaVMSpecificationName() {
+ return getProperty(StandardSystemProperty.JAVA_VM_SPECIFICATION_NAME);
+ }
+
+ public String getJavaVMVersion() {
+ return getProperty(StandardSystemProperty.JAVA_VM_VERSION);
+ }
+
+ public String getJavaVMVendor() {
+ return getProperty(StandardSystemProperty.JAVA_VM_VENDOR);
+ }
+
+ public String getJavaVMName() {
+ return getProperty(StandardSystemProperty.JAVA_VM_NAME);
+ }
+
+ public String getJavaSpecificationVersion() {
+ return getProperty(StandardSystemProperty.JAVA_SPECIFICATION_VERSION);
+ }
+
+ public String getJavaSpecificationVendor() {
+ return getProperty(StandardSystemProperty.JAVA_SPECIFICATION_VENDOR);
+ }
+
+ public String getJavaSpecificationName() {
+ return getProperty(StandardSystemProperty.JAVA_SPECIFICATION_NAME);
+ }
+
+ public String getJavaClassVersion() {
+ return getProperty(StandardSystemProperty.JAVA_CLASS_VERSION);
+ }
+
+ public String getJavaCompiler() {
+ return getProperty(StandardSystemProperty.JAVA_COMPILER);
+ }
+
+ public String getPlatform() {
+ return getProperty(StandardSystemProperty.OS_ARCH);
+ }
+
+ public String getOSName() {
+ return getProperty(StandardSystemProperty.OS_NAME);
+ }
+
+ public String getOSArch() {
+ return getProperty(StandardSystemProperty.OS_ARCH);
+ }
+
+ public String getOSVersion() {
+ return getProperty(StandardSystemProperty.OS_VERSION);
+ }
+
+ private String getProperty(final StandardSystemProperty standardKey) {
+ return getSanitizedString(props.getProperty(standardKey.key(), UNKNOWN));
+ }
+
+ private String getSanitizedString(final String string) {
+ return Strings.isNullOrEmpty(string) ? UNKNOWN : string.trim();
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/updatechecker/ProductInfo.java b/server/src/main/java/org/killbill/billing/server/updatechecker/ProductInfo.java
new file mode 100644
index 0000000..ce16651
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/updatechecker/ProductInfo.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.updatechecker;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.Properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Strings;
+import com.google.common.io.InputSupplier;
+import com.google.common.io.Resources;
+
+/**
+ * Kill Bill specific information
+ * <p/>
+ * At build time, we generated a magic file (version.properties) which should be on the classpath.
+ */
+public class ProductInfo {
+
+ private static final Logger log = LoggerFactory.getLogger(ProductInfo.class);
+
+ private static final String KILLBILL_SERVER_VERSION_RESOURCE = "/org/killbill/billing/server/version.properties";
+
+ private static final String UNKNOWN = "UNKNOWN";
+
+ private static final String PRODUCT_NAME = "product-name";
+ private static final String VERSION = "version";
+ private static final String BUILT_BY = "built-by";
+ private static final String BUILD_JDK = "build-jdk";
+ private static final String BUILD_TIME = "build-time";
+ private static final String ENTERPRISE = "enterprise";
+
+ private final Properties props = new Properties();
+
+ public ProductInfo() {
+ try {
+ parseProductInfo(KILLBILL_SERVER_VERSION_RESOURCE);
+ } catch (IOException e) {
+ log.debug("Unable to detect current product info", e);
+ }
+ }
+
+ private void parseProductInfo(final String resource) throws IOException {
+ final URL resourceURL = Resources.getResource(resource);
+ final InputSupplier<InputStreamReader> inputSupplier = Resources.newReaderSupplier(resourceURL, Charset.forName("UTF-8"));
+ props.load(inputSupplier.getInput());
+ }
+
+ public String getName() {
+ return getProperty(PRODUCT_NAME);
+ }
+
+ public String getVersion() {
+ return getProperty(VERSION);
+ }
+
+ public String getBuiltBy() {
+ return getProperty(BUILT_BY);
+ }
+
+ public String getBuildJdk() {
+ return getProperty(BUILD_JDK);
+ }
+
+ public String getBuildTime() {
+ return getProperty(BUILD_TIME);
+ }
+
+ public boolean isEnterprise() {
+ return Boolean.parseBoolean(props.getProperty(ENTERPRISE));
+ }
+
+ private String getProperty(final String key) {
+ return getSanitizedString(props.getProperty(key, UNKNOWN));
+ }
+
+ private String getSanitizedString(final String string) {
+ return Strings.isNullOrEmpty(string) ? UNKNOWN : string.trim();
+ }
+
+ @Override
+ public String toString() {
+ final String fullProductName = String.format("%s (%s)", getName(), isEnterprise() ? "enterprise" : "community");
+ return String.format("%s version %s was built on %s, with jdk %s by %s",
+ fullProductName, getVersion(), getBuildTime(), getBuildJdk(), getBuiltBy());
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/updatechecker/Tracker.java b/server/src/main/java/org/killbill/billing/server/updatechecker/Tracker.java
new file mode 100644
index 0000000..f56f4fd
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/updatechecker/Tracker.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.updatechecker;
+
+import javax.servlet.ServletContext;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.server.config.UpdateCheckConfig;
+
+import com.dmurph.tracking.AnalyticsConfigData;
+import com.dmurph.tracking.JGoogleAnalyticsTracker;
+import com.dmurph.tracking.JGoogleAnalyticsTracker.GoogleAnalyticsVersion;
+
+public class Tracker {
+
+ private static final Logger log = LoggerFactory.getLogger(Tracker.class);
+ private static final String TRACKING_CODE = "UA-44821278-1";
+
+ // Information about this version of Kill Bill
+ final ProductInfo productInfo;
+ // Information about this JVM
+ private final ClientInfo clientInfo;
+ private final JGoogleAnalyticsTracker tracker;
+
+ public Tracker(final ProductInfo productInfo, final ServletContext context) {
+ this.productInfo = productInfo;
+ this.clientInfo = new ClientInfo(context);
+
+ final AnalyticsConfigData analyticsConfigData = new AnalyticsConfigData(TRACKING_CODE);
+ this.tracker = new JGoogleAnalyticsTracker(analyticsConfigData, GoogleAnalyticsVersion.V_4_7_2);
+ }
+
+ public void track() {
+ trackProperty("product", "name", productInfo.getName());
+ trackProperty("product", "version", productInfo.getVersion());
+ trackProperty("product", "builtBy", productInfo.getBuiltBy());
+ trackProperty("product", "buildJdk", productInfo.getBuildJdk());
+ trackProperty("product", "buildTime", productInfo.getBuildTime());
+ trackProperty("product", "enterprise", String.valueOf(productInfo.isEnterprise()));
+
+ trackProperty("client", "servletMajorVersion", clientInfo.getServletMajorVersion());
+ trackProperty("client", "servletMinorVersion", clientInfo.getServletMinorVersion());
+ trackProperty("client", "servletEffectiveMajorVersion", clientInfo.getServletEffectiveMajorVersion());
+ trackProperty("client", "servletEffectiveMinorVersion", clientInfo.getServletEffectiveMinorVersion());
+ trackProperty("client", "serverInfo", clientInfo.getServerInfo());
+ trackProperty("client", "clientId", clientInfo.getClientId());
+ trackProperty("client", "javaVersion", clientInfo.getJavaVersion());
+ trackProperty("client", "javaVendor", clientInfo.getJavaVendor());
+ trackProperty("client", "javaVendorURL", clientInfo.getJavaVendorURL());
+ trackProperty("client", "javaVMSpecificationVersion", clientInfo.getJavaVMSpecificationVersion());
+ trackProperty("client", "javaVMSpecificationVendor", clientInfo.getJavaVMSpecificationVendor());
+ trackProperty("client", "javaVMSpecificationName", clientInfo.getJavaVMSpecificationName());
+ trackProperty("client", "javaVMVersion", clientInfo.getJavaVMVersion());
+ trackProperty("client", "javaVMVendor", clientInfo.getJavaVMVendor());
+ trackProperty("client", "javaVMName", clientInfo.getJavaVMName());
+ trackProperty("client", "javaSpecificationVersion", clientInfo.getJavaSpecificationVersion());
+ trackProperty("client", "javaSpecificationVendor", clientInfo.getJavaSpecificationVendor());
+ trackProperty("client", "javaSpecificationName", clientInfo.getJavaSpecificationName());
+ trackProperty("client", "javaClassVersion", clientInfo.getJavaClassVersion());
+ trackProperty("client", "javaCompiler", clientInfo.getJavaCompiler());
+ trackProperty("client", "platform", clientInfo.getPlatform());
+ trackProperty("client", "osName", clientInfo.getOSName());
+ trackProperty("client", "osArch", clientInfo.getOSArch());
+ trackProperty("client", "osVersion", clientInfo.getOSVersion());
+ }
+
+ private void trackProperty(final String category, final String key, final String value) {
+ // Workaround for https://code.google.com/p/analytics-issues/issues/detail?id=219
+ String sanitizedValue = value;
+ sanitizedValue = sanitizedValue.replace('(', '-');
+ sanitizedValue = sanitizedValue.replace(')', '-');
+
+ log.debug("Tracking {}: {}={}", category, key, sanitizedValue);
+ tracker.trackEvent(category, key, sanitizedValue);
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/updatechecker/UpdateChecker.java b/server/src/main/java/org/killbill/billing/server/updatechecker/UpdateChecker.java
new file mode 100644
index 0000000..a19326f
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/updatechecker/UpdateChecker.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.updatechecker;
+
+import java.io.IOException;
+
+import javax.servlet.ServletContext;
+
+import org.skife.config.ConfigurationObjectFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.server.config.UpdateCheckConfig;
+
+public class UpdateChecker {
+
+ private static final Logger log = LoggerFactory.getLogger(UpdateChecker.class);
+
+ final UpdateCheckConfig config = new ConfigurationObjectFactory(System.getProperties()).build(UpdateCheckConfig.class);
+
+ public void check(final ServletContext servletContext) {
+ log.info("For Kill Bill Commercial Support, visit http://thebillingproject.com or send an email to support@thebillingproject.com");
+
+ if (shouldSkipUpdateCheck()) {
+ return;
+ }
+
+ final Thread t = new Thread() {
+ @Override
+ public void run() {
+ try {
+ doCheck(servletContext);
+ } catch (IOException e) {
+ // Don't pollute logs, maybe no internet access?
+ log.debug("Unable to perform update check", e);
+ }
+ }
+ };
+ t.setDaemon(true);
+ t.start();
+ }
+
+ private void doCheck(final ServletContext servletContext) throws IOException {
+ // Information about this version of Kill Bill
+ final ProductInfo productInfo = new ProductInfo();
+ // Information about other versions of Kill Bill
+ final UpdateListProperties updateListProperties = new UpdateListProperties(config.updateCheckURL().toURL(), config.updateCheckConnectionTimeout());
+
+ // Log generic information about Kill Bill
+ if (updateListProperties.getGeneralNotice() != null) {
+ log.info(updateListProperties.getGeneralNotice());
+ }
+
+ // Log generic information about this release
+ if (updateListProperties.getNoticeForVersion(productInfo.getVersion()) != null) {
+ log.info(updateListProperties.getNoticeForVersion(productInfo.getVersion()));
+ }
+
+ // Log if there is a new version of Kill Bill available
+ final StringBuilder updates = new StringBuilder();
+ for (final String update : updateListProperties.getUpdatesForVersion(productInfo.getVersion())) {
+ if (updates.length() > 0) {
+ updates.append(", ");
+ }
+
+ updates.append(update);
+ final String changeLog = updateListProperties.getReleaseNotesForVersion(update);
+ if (changeLog != null) {
+ updates.append(" [").append(changeLog).append("]");
+ }
+ }
+ if (updates.length() > 0) {
+ log.info("New update(s) found: " + updates.toString() + ". Please check http://kill-bill.org for the latest version.");
+ }
+
+ // Send anonymous data
+ final Tracker tracker = new Tracker(productInfo, servletContext);
+ tracker.track();
+ }
+
+
+ private boolean shouldSkipUpdateCheck() {
+ if (config.shouldSkipUpdateCheck()) {
+ return true;
+ }
+
+ try {
+ Class.forName("org.testng.Assert");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+}
diff --git a/server/src/main/java/org/killbill/billing/server/updatechecker/UpdateListProperties.java b/server/src/main/java/org/killbill/billing/server/updatechecker/UpdateListProperties.java
new file mode 100644
index 0000000..86b66f1
--- /dev/null
+++ b/server/src/main/java/org/killbill/billing/server/updatechecker/UpdateListProperties.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.updatechecker;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
+public class UpdateListProperties {
+
+ private static final Logger log = LoggerFactory.getLogger(UpdateListProperties.class);
+
+ private static final Splitter SPLITTER = Splitter.on(",").trimResults().omitEmptyStrings();
+
+ private final Properties properties = new Properties();
+
+ public UpdateListProperties(final URL updateCheckURL, final int connectionTimeout) {
+ try {
+ loadUpdateListProperties(updateCheckURL, connectionTimeout);
+ } catch (IOException e) {
+ log.debug("Unable to load update list properties", e);
+ }
+ }
+
+ public String getGeneralNotice() {
+ return getProperty("general.notice");
+ }
+
+ public String getNoticeForVersion(final String version) {
+ return getProperty(version + ".notice");
+ }
+
+ public List<String> getUpdatesForVersion(final String version) {
+ final String updates = getProperty(version + ".updates");
+ return updates == null ? ImmutableList.<String>of() : SPLITTER.splitToList(updates);
+ }
+
+ public String getReleaseNotesForVersion(final String version) {
+ return getProperty(version + ".release-notes");
+ }
+
+ private String getProperty(final String key) {
+ return getSanitizedString(properties.getProperty(key));
+ }
+
+ private String getSanitizedString(final String string) {
+ return Strings.isNullOrEmpty(string) ? null : string.trim();
+ }
+
+ private void loadUpdateListProperties(final URL updateCheckURL, final int connectionTimeout) throws IOException {
+ log.debug("Checking {} for updates", updateCheckURL.toExternalForm());
+ final URLConnection connection = updateCheckURL.openConnection();
+ connection.setConnectTimeout(connectionTimeout);
+
+ final InputStream in = connection.getInputStream();
+ try {
+ properties.load(in);
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ }
+}
server/src/main/resources/ehcache.xml 10(+5 -5)
diff --git a/server/src/main/resources/ehcache.xml b/server/src/main/resources/ehcache.xml
index 07e2a73..fb82f50 100644
--- a/server/src/main/resources/ehcache.xml
+++ b/server/src/main/resources/ehcache.xml
@@ -39,7 +39,7 @@
statistics="true"
>
<cacheEventListenerFactory
- class="com.ning.billing.util.cache.ExpirationListenerFactory"
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
properties=""/>
</cache>
@@ -53,7 +53,7 @@
statistics="true"
>
<cacheEventListenerFactory
- class="com.ning.billing.util.cache.ExpirationListenerFactory"
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
properties=""/>
</cache>
@@ -67,7 +67,7 @@
statistics="true"
>
<cacheEventListenerFactory
- class="com.ning.billing.util.cache.ExpirationListenerFactory"
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
properties=""/>
</cache>
@@ -82,7 +82,7 @@
statistics="true"
>
<cacheEventListenerFactory
- class="com.ning.billing.util.cache.ExpirationListenerFactory"
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
properties=""/>
</cache>
@@ -97,7 +97,7 @@
statistics="true"
>
<cacheEventListenerFactory
- class="com.ning.billing.util.cache.ExpirationListenerFactory"
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
properties=""/>
</cache>
</ehcache>
diff --git a/server/src/main/resources/killbill-server.properties b/server/src/main/resources/killbill-server.properties
index 49012e2..27b7c9c 100644
--- a/server/src/main/resources/killbill-server.properties
+++ b/server/src/main/resources/killbill-server.properties
@@ -16,12 +16,12 @@
# Use skeleton properties for server and configure killbill database
-com.ning.jetty.jdbi.url=jdbc:mysql://127.0.0.1:3306/killbill
-com.ning.jetty.jdbi.user=root
-com.ning.jetty.jdbi.password=root
+org.killbill.dao.url=jdbc:h2:file:killbill;MODE=MYSQL;DB_CLOSE_DELAY=-1;MVCC=true;DB_CLOSE_ON_EXIT=FALSE
+org.killbill.dao.user=root
+org.killbill.dao.password=root
# Use the SpyCarAdvanced.xml catalog
-killbill.catalog.uri=SpyCarAdvanced.xml
+org.killbill.catalog.uri=SpyCarAdvanced.xml
# Set default timezone to UTC
user.timezone=UTC
@@ -30,20 +30,20 @@ user.timezone=UTC
ANTLR_USE_DIRECT_CLASS_LOADING=true
# To enable test endpoint and have Kill Bill run with a ClockMock
-killbill.server.test.mode=true
+org.killbill.server.test.mode=true
-killbill.billing.notificationq.main.sleep=100
+org.killbill.notificationq.main.sleep=100
-killbill.billing.persistent.bus.main.sleep=100
-killbill.billing.persistent.bus.main.nbThreads=1
-killbill.billing.persistent.bus.main.claimed=1
+org.killbill.persistent.bus.main.sleep=100
+org.killbill.persistent.bus.main.nbThreads=1
+org.killbill.persistent.bus.main.claimed=1
-killbill.billing.persistent.bus.external.sleep=100
-killbill.billing.persistent.bus.external.nbThreads=1
-killbill.billing.persistent.bus.external.claimed=1
-killbill.billing.persistent.bus.external.tableName=bus_ext_events
-killbill.billing.persistent.bus.external.historyTableName=bus_ext_events_history
+org.killbill.persistent.bus.external.sleep=100
+org.killbill.persistent.bus.external.nbThreads=1
+org.killbill.persistent.bus.external.claimed=1
+org.killbill.persistent.bus.external.tableName=bus_ext_events
+org.killbill.persistent.bus.external.historyTableName=bus_ext_events_history
-killbill.server.multitenant=false
+org.killbill.server.multitenant=false
diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml
index 8649b66..7de0d93 100644
--- a/server/src/main/resources/logback.xml
+++ b/server/src/main/resources/logback.xml
@@ -25,9 +25,9 @@
<!-- Silence verbose loggers in DEBUG mode -->
<logger name="com.dmurph" level="INFO"/>
- <logger name="com.ning.billing.notificationq" level="INFO"/>
- <logger name="com.ning.billing.queue" level="INFO"/>
- <logger name="com.ning.billing.server.updatechecker" level="INFO"/>
+ <logger name="org.killbill.billing.notificationq" level="INFO"/>
+ <logger name="org.killbill.billing.queue" level="INFO"/>
+ <logger name="org.killbill.billing.server.updatechecker" level="INFO"/>
<logger name="org.eclipse" level="INFO"/>
<root level="INFO">
diff --git a/server/src/main/resources/org/killbill/billing/server/version.properties b/server/src/main/resources/org/killbill/billing/server/version.properties
new file mode 100644
index 0000000..3ea3ec3
--- /dev/null
+++ b/server/src/main/resources/org/killbill/billing/server/version.properties
@@ -0,0 +1,6 @@
+product-name = ${project.name}
+version = ${project.version}
+built-by = ${user.name}
+build-jdk = ${java.version}
+build-time = ${build.timestamp}
+enterprise = false
server/src/main/resources/shiro.ini 4(+2 -2)
diff --git a/server/src/main/resources/shiro.ini b/server/src/main/resources/shiro.ini
index a419d0a..78798a9 100644
--- a/server/src/main/resources/shiro.ini
+++ b/server/src/main/resources/shiro.ini
@@ -17,11 +17,11 @@
###################################################################################
# [main]
-# See com.ning.billing.util.glue.KillBillShiroModule
+# See org.killbill.billing.util.glue.KillBillShiroModule
# Use -Dkillbill.server.rbac=false to disable RBAC
# Default admin user
-# Use -Dkillbill.security.shiroResourcePath=/var/tmp/shiro.ini to specify your own config
+# Use -Dorg.killbill.security.shiroResourcePath=/var/tmp/shiro.ini to specify your own config
[users]
admin = password, root
diff --git a/server/src/main/resources/update-checker/killbill-server-update-list.properties b/server/src/main/resources/update-checker/killbill-server-update-list.properties
index c1ce057..cec6222 100644
--- a/server/src/main/resources/update-checker/killbill-server-update-list.properties
+++ b/server/src/main/resources/update-checker/killbill-server-update-list.properties
@@ -1,7 +1,16 @@
## Top level keys
# general.notice = This notice should rarely, if ever, be used as everyone will see it
-## 0.8.13 -- latest release
+### 0.9.x series ###
+
+## 0.9.1 -- latest release
+0.9.1.updates =
+0.9.1.notices = This is an unstable release
+0.9.1.release-notes = http://kill-bill.org
+
+### 0.8.x series ###
+
+## 0.8.13 -- latest stable release
0.8.13.updates =
0.8.13.notices = This is the latest GA release.
0.8.13.release-notes = http://kill-bill.org
diff --git a/server/src/main/webapp/WEB-INF/web.xml b/server/src/main/webapp/WEB-INF/web.xml
index f5fac62..f941220 100644
--- a/server/src/main/webapp/WEB-INF/web.xml
+++ b/server/src/main/webapp/WEB-INF/web.xml
@@ -38,7 +38,7 @@
<filter>
<!-- Guice emulates Servlet API with DI -->
<filter-name>guiceFilter</filter-name>
- <filter-class>com.ning.billing.server.filters.KillbillGuiceFilter</filter-class>
+ <filter-class>org.killbill.billing.server.filters.KillbillGuiceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>guiceFilter</filter-name>
@@ -46,11 +46,11 @@
</filter-mapping>
<listener>
<!-- Jersey insists on using java.util.logging (JUL) -->
- <listener-class>com.ning.jetty.core.listeners.SetupJULBridge</listener-class>
+ <listener-class>org.killbill.commons.skeleton.listeners.JULServletContextListener</listener-class>
</listener>
<listener>
<!-- Context listener: called at startup time and creates the injector -->
- <listener-class>com.ning.billing.server.listeners.KillbillGuiceListener</listener-class>
+ <listener-class>org.killbill.billing.server.listeners.KillbillGuiceListener</listener-class>
</listener>
<!-- ServletHandler#handle requires a backend servlet. Besides, this will also be used to serve static resources,
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java b/server/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
new file mode 100644
index 0000000..4d23a71
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.client.KillBillClient;
+import org.killbill.billing.client.KillBillHttpClient;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.PaymentMethod;
+import org.killbill.billing.client.model.PaymentMethodPluginDetail;
+import org.killbill.billing.client.model.PaymentMethodProperties;
+import org.killbill.billing.client.model.Subscription;
+
+import static org.testng.Assert.assertNotNull;
+
+public abstract class KillbillClient extends GuicyKillbillTestSuiteWithEmbeddedDB {
+
+ protected static final String PLUGIN_NAME = "noop";
+
+ protected static final String DEFAULT_CURRENCY = "USD";
+
+ // Multi-Tenancy information, if enabled
+ protected String DEFAULT_API_KEY = UUID.randomUUID().toString();
+ protected String DEFAULT_API_SECRET = UUID.randomUUID().toString();
+
+ // RBAC information, if enabled
+ protected String USERNAME = "tester";
+ protected String PASSWORD = "tester";
+
+ // Context information to be passed around
+ protected static final String createdBy = "Toto";
+ protected static final String reason = "i am god";
+ protected static final String comment = "no comment";
+
+ protected KillBillClient killBillClient;
+ protected KillBillHttpClient killBillHttpClient;
+
+ protected List<PaymentMethodProperties> getPaymentMethodCCProperties() {
+ final List<PaymentMethodProperties> properties = new ArrayList<PaymentMethodProperties>();
+ properties.add(new PaymentMethodProperties("type", "CreditCard", false));
+ properties.add(new PaymentMethodProperties("cardType", "Visa", false));
+ properties.add(new PaymentMethodProperties("cardHolderName", "Mr Sniff", false));
+ properties.add(new PaymentMethodProperties("expirationDate", "2015-08", false));
+ properties.add(new PaymentMethodProperties("maskNumber", "3451", false));
+ properties.add(new PaymentMethodProperties("address1", "23, rue des cerisiers", false));
+ properties.add(new PaymentMethodProperties("address2", "", false));
+ properties.add(new PaymentMethodProperties("city", "Toulouse", false));
+ properties.add(new PaymentMethodProperties("country", "France", false));
+ properties.add(new PaymentMethodProperties("postalCode", "31320", false));
+ properties.add(new PaymentMethodProperties("state", "Midi-Pyrenees", false));
+ return properties;
+ }
+
+ protected List<PaymentMethodProperties> getPaymentMethodPaypalProperties() {
+ final List<PaymentMethodProperties> properties = new ArrayList<PaymentMethodProperties>();
+ properties.add(new PaymentMethodProperties("type", "CreditCard", false));
+ properties.add(new PaymentMethodProperties("email", "zouzou@laposte.fr", false));
+ properties.add(new PaymentMethodProperties("baid", "23-8787d-R", false));
+ return properties;
+ }
+
+ protected Account createAccountWithDefaultPaymentMethod() throws Exception {
+ final Account input = createAccount();
+
+ final PaymentMethodPluginDetail info = new PaymentMethodPluginDetail();
+ final PaymentMethod paymentMethodJson = new PaymentMethod(null, input.getAccountId(), true, PLUGIN_NAME, info);
+ killBillClient.createPaymentMethod(paymentMethodJson, createdBy, reason, comment);
+
+ return killBillClient.getAccount(input.getExternalKey());
+ }
+
+ protected Account createAccount() throws Exception {
+ final Account input = getAccount();
+ return killBillClient.createAccount(input, createdBy, reason, comment);
+ }
+
+ protected Subscription createEntitlement(final UUID accountId, final String bundleExternalKey, final String productName,
+ final ProductCategory productCategory, final BillingPeriod billingPeriod, final boolean waitCompletion) throws Exception {
+ final Subscription input = new Subscription();
+ input.setAccountId(accountId);
+ input.setExternalKey(bundleExternalKey);
+ input.setProductName(productName);
+ input.setProductCategory(productCategory);
+ input.setBillingPeriod(billingPeriod);
+ input.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ return killBillClient.createSubscription(input, waitCompletion ? 5 : -1, createdBy, reason, comment);
+ }
+
+ protected Account createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice() throws Exception {
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+ assertNotNull(accountJson);
+
+ // Add a bundle, subscription and move the clock to get the first invoice
+ final Subscription subscriptionJson = createEntitlement(accountJson.getAccountId(), UUID.randomUUID().toString(), "Shotgun",
+ ProductCategory.BASE, BillingPeriod.MONTHLY, true);
+ assertNotNull(subscriptionJson);
+ clock.addDays(32);
+ crappyWaitForLackOfProperSynchonization();
+
+ return accountJson;
+ }
+
+ protected Account createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice() throws Exception {
+ // Create an account with no payment method
+ final Account accountJson = createAccount();
+ assertNotNull(accountJson);
+
+ // Add a bundle, subscription and move the clock to get the first invoice
+ final Subscription subscriptionJson = createEntitlement(accountJson.getAccountId(), UUID.randomUUID().toString(), "Shotgun",
+ ProductCategory.BASE, BillingPeriod.MONTHLY, true);
+ assertNotNull(subscriptionJson);
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+
+ // No payment will be triggered as the account doesn't have a payment method
+
+ return accountJson;
+ }
+
+ protected Account getAccount() {
+ return getAccount(UUID.randomUUID().toString(), UUID.randomUUID().toString(), UUID.randomUUID().toString().substring(0, 5) + '@' + UUID.randomUUID().toString().substring(0, 5));
+ }
+
+ public Account getAccount(final String name, final String externalKey, final String email) {
+ final UUID accountId = UUID.randomUUID();
+ final int length = 4;
+ final String currency = DEFAULT_CURRENCY;
+ final String timeZone = "UTC";
+ final String address1 = "12 rue des ecoles";
+ final String address2 = "Poitier";
+ final String postalCode = "44 567";
+ final String company = "Renault";
+ final String city = "Quelque part";
+ final String state = "Poitou";
+ final String country = "France";
+ final String locale = "fr";
+ final String phone = "81 53 26 56";
+
+ // Note: the accountId payload is ignored on account creation
+ return new Account(accountId, name, length, externalKey, email, null, currency, null, timeZone,
+ address1, address2, postalCode, company, city, state, country, locale, phone, false, false, null, null);
+ }
+
+ /**
+ * We could implement a ClockResource in jaxrs with the ability to sync on user token
+ * but until we have a strong need for it, this is in the TODO list...
+ */
+ protected void crappyWaitForLackOfProperSynchonization() throws Exception {
+ Thread.sleep(5000);
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestAccount.java b/server/src/test/java/org/killbill/billing/jaxrs/TestAccount.java
new file mode 100644
index 0000000..ad6483c
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestAccount.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Accounts;
+import org.killbill.billing.client.model.AuditLog;
+import org.killbill.billing.client.model.CustomField;
+import org.killbill.billing.client.model.Payment;
+import org.killbill.billing.client.model.PaymentMethod;
+import org.killbill.billing.client.model.PaymentMethodPluginDetail;
+import org.killbill.billing.client.model.Refund;
+import org.killbill.billing.client.model.Tag;
+import org.killbill.billing.util.api.AuditLevel;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class TestAccount extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can create, retrieve, search and update accounts")
+ public void testAccountOk() throws Exception {
+ final Account input = createAccount();
+
+ // Retrieves by external key
+ final Account retrievedAccount = killBillClient.getAccount(input.getExternalKey());
+ Assert.assertTrue(retrievedAccount.equals(input));
+
+ // Try search endpoint
+ searchAccount(input, retrievedAccount);
+
+ // Update Account
+ final Account newInput = new Account(input.getAccountId(),
+ "zozo", 4, input.getExternalKey(), "rr@google.com", 18,
+ "USD", null, "UTC", "bl1", "bh2", "", "", "ca", "San Francisco", "usa", "en", "415-255-2991",
+ false, false, null, null);
+ final Account updatedAccount = killBillClient.updateAccount(newInput, createdBy, reason, comment);
+ Assert.assertTrue(updatedAccount.equals(newInput));
+
+ // Try search endpoint
+ searchAccount(input, null);
+ }
+
+ @Test(groups = "slow", description = "Can retrieve the account balance")
+ public void testAccountWithBalance() throws Exception {
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ final Account accountWithBalance = killBillClient.getAccount(accountJson.getAccountId(), true, false);
+ final BigDecimal accountBalance = accountWithBalance.getAccountBalance();
+ Assert.assertTrue(accountBalance.compareTo(BigDecimal.ZERO) > 0);
+ }
+
+ @Test(groups = "slow", description = "Cannot update a non-existent account")
+ public void testUpdateNonExistentAccount() throws Exception {
+ final Account input = getAccount();
+
+ Assert.assertNull(killBillClient.updateAccount(input, createdBy, reason, comment));
+ }
+
+ @Test(groups = "slow", description = "Cannot retrieve non-existent account")
+ public void testAccountNonExistent() throws Exception {
+ Assert.assertNull(killBillClient.getAccount(UUID.randomUUID()));
+ Assert.assertNull(killBillClient.getAccount(UUID.randomUUID().toString()));
+ }
+
+ @Test(groups = "slow", description = "Can CRUD payment methods")
+ public void testAccountPaymentMethods() throws Exception {
+ final Account accountJson = createAccount();
+ assertNotNull(accountJson);
+
+ final PaymentMethodPluginDetail info = new PaymentMethodPluginDetail();
+ info.setProperties(getPaymentMethodCCProperties());
+ PaymentMethod paymentMethodJson = new PaymentMethod(null, accountJson.getAccountId(), true, PLUGIN_NAME, info);
+ final PaymentMethod paymentMethodCC = killBillClient.createPaymentMethod(paymentMethodJson, createdBy, reason, comment);
+ assertTrue(paymentMethodCC.getIsDefault());
+
+ //
+ // Add another payment method
+ //
+ final PaymentMethodPluginDetail info2 = new PaymentMethodPluginDetail();
+ info2.setProperties(getPaymentMethodPaypalProperties());
+ paymentMethodJson = new PaymentMethod(null, accountJson.getAccountId(), false, PLUGIN_NAME, info2);
+ final PaymentMethod paymentMethodPP = killBillClient.createPaymentMethod(paymentMethodJson, createdBy, reason, comment);
+ assertFalse(paymentMethodPP.getIsDefault());
+
+ //
+ // FETCH ALL PAYMENT METHODS
+ //
+ List<PaymentMethod> paymentMethods = killBillClient.getPaymentMethodsForAccount(accountJson.getAccountId());
+ assertEquals(paymentMethods.size(), 2);
+
+ //
+ // CHANGE DEFAULT
+ //
+ assertTrue(killBillClient.getPaymentMethod(paymentMethodCC.getPaymentMethodId()).getIsDefault());
+ assertFalse(killBillClient.getPaymentMethod(paymentMethodPP.getPaymentMethodId()).getIsDefault());
+ killBillClient.updateDefaultPaymentMethod(accountJson.getAccountId(), paymentMethodPP.getPaymentMethodId(), createdBy, reason, comment);
+ assertTrue(killBillClient.getPaymentMethod(paymentMethodPP.getPaymentMethodId()).getIsDefault());
+ assertFalse(killBillClient.getPaymentMethod(paymentMethodCC.getPaymentMethodId()).getIsDefault());
+
+ //
+ // DELETE NON DEFAULT PM
+ //
+ killBillClient.deletePaymentMethod(paymentMethodCC.getPaymentMethodId(), false, createdBy, reason, comment);
+
+ //
+ // FETCH ALL PAYMENT METHODS
+ //
+ paymentMethods = killBillClient.getPaymentMethodsForAccount(accountJson.getAccountId());
+ assertEquals(paymentMethods.size(), 1);
+
+ //
+ // DELETE DEFAULT PAYMENT METHOD (without special flag first)
+ //
+ try {
+ killBillClient.deletePaymentMethod(paymentMethodPP.getPaymentMethodId(), false, createdBy, reason, comment);
+ fail();
+ } catch (final KillBillClientException e) {
+ }
+
+ //
+ // RETRY TO DELETE DEFAULT PAYMENT METHOD (with special flag this time)
+ //
+ killBillClient.deletePaymentMethod(paymentMethodPP.getPaymentMethodId(), true, createdBy, reason, comment);
+
+ // CHECK ACCOUNT IS NOW AUTO_PAY_OFF
+ final List<Tag> tagsJson = killBillClient.getAccountTags(accountJson.getAccountId());
+ Assert.assertEquals(tagsJson.size(), 1);
+ final Tag tagJson = tagsJson.get(0);
+ Assert.assertEquals(tagJson.getTagDefinitionName(), "AUTO_PAY_OFF");
+ Assert.assertEquals(tagJson.getTagDefinitionId(), new UUID(0, 1));
+
+ // FETCH ACCOUNT AGAIN AND CHECK THERE IS NO DEFAULT PAYMENT METHOD SET
+ final Account updatedAccount = killBillClient.getAccount(accountJson.getAccountId());
+ Assert.assertEquals(updatedAccount.getAccountId(), accountJson.getAccountId());
+ Assert.assertNull(updatedAccount.getPaymentMethodId());
+
+ //
+ // FINALLY TRY TO REMOVE AUTO_PAY_OFF WITH NO DEFAULT PAYMENT METHOD ON ACCOUNT
+ //
+ try {
+ killBillClient.deleteAccountTag(accountJson.getAccountId(), new UUID(0, 1), createdBy, reason, comment);
+ } catch (final KillBillClientException e) {
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testAccountPaymentsWithRefund() throws Exception {
+ final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Verify payments
+ final List<Payment> objFromJson = killBillClient.getPaymentsForAccount(accountJson.getAccountId());
+ Assert.assertEquals(objFromJson.size(), 1);
+
+ // Verify refunds
+ final List<Refund> objRefundFromJson = killBillClient.getRefundsForAccount(accountJson.getAccountId());
+ Assert.assertEquals(objRefundFromJson.size(), 0);
+ }
+
+ @Test(groups = "slow", description = "Add tags to account")
+ public void testTags() throws Exception {
+ final Account input = createAccount();
+ // Use tag definition for AUTO_PAY_OFF
+ final UUID autoPayOffId = new UUID(0, 1);
+
+ // Add a tag
+ killBillClient.createAccountTag(input.getAccountId(), autoPayOffId, createdBy, reason, comment);
+
+ // Retrieves all tags
+ final List<Tag> tags1 = killBillClient.getAccountTags(input.getAccountId(), AuditLevel.FULL);
+ Assert.assertEquals(tags1.size(), 1);
+ Assert.assertEquals(tags1.get(0).getTagDefinitionId(), autoPayOffId);
+
+ // Verify adding the same tag a second time doesn't do anything
+ killBillClient.createAccountTag(input.getAccountId(), autoPayOffId, createdBy, reason, comment);
+
+ // Retrieves all tags again
+ killBillClient.createAccountTag(input.getAccountId(), autoPayOffId, createdBy, reason, comment);
+ final List<Tag> tags2 = killBillClient.getAccountTags(input.getAccountId(), AuditLevel.FULL);
+ Assert.assertEquals(tags2, tags1);
+
+ // Verify audit logs
+ Assert.assertEquals(tags2.get(0).getAuditLogs().size(), 1);
+ final AuditLog auditLogJson = tags2.get(0).getAuditLogs().get(0);
+ Assert.assertEquals(auditLogJson.getChangeType(), "INSERT");
+ Assert.assertEquals(auditLogJson.getChangedBy(), createdBy);
+ Assert.assertEquals(auditLogJson.getReasonCode(), reason);
+ Assert.assertEquals(auditLogJson.getComments(), comment);
+ Assert.assertNotNull(auditLogJson.getChangeDate());
+ Assert.assertNotNull(auditLogJson.getUserToken());
+ }
+
+ @Test(groups = "slow", description = "Add custom fields to account")
+ public void testCustomFields() throws Exception {
+ final Account accountJson = createAccount();
+ assertNotNull(accountJson);
+
+ final Collection<CustomField> customFields = new LinkedList<CustomField>();
+ customFields.add(new CustomField(null, accountJson.getAccountId(), ObjectType.ACCOUNT, "1", "value1", null));
+ customFields.add(new CustomField(null, accountJson.getAccountId(), ObjectType.ACCOUNT, "2", "value2", null));
+ customFields.add(new CustomField(null, accountJson.getAccountId(), ObjectType.ACCOUNT, "3", "value3", null));
+
+ killBillClient.createAccountCustomFields(accountJson.getAccountId(), customFields, createdBy, reason, comment);
+
+ final List<CustomField> accountCustomFields = killBillClient.getAccountCustomFields(accountJson.getAccountId());
+ assertEquals(accountCustomFields.size(), 3);
+
+ // Delete all custom fields for account
+ killBillClient.deleteAccountCustomFields(accountJson.getAccountId(), createdBy, reason, comment);
+
+ final List<CustomField> remainingCustomFields = killBillClient.getAccountCustomFields(accountJson.getAccountId());
+ assertEquals(remainingCustomFields.size(), 0);
+ }
+
+ @Test(groups = "slow", description = "Can paginate through all accounts")
+ public void testAccountsPagination() throws Exception {
+ for (int i = 0; i < 5; i++) {
+ createAccount();
+ }
+
+ final Accounts allAccounts = killBillClient.getAccounts();
+ Assert.assertEquals(allAccounts.size(), 5);
+
+ Accounts page = killBillClient.getAccounts(0L, 1L);
+ for (int i = 0; i < 5; i++) {
+ Assert.assertNotNull(page);
+ Assert.assertEquals(page.size(), 1);
+ Assert.assertEquals(page.get(0), allAccounts.get(i));
+ page = page.getNext();
+ }
+ Assert.assertNull(page);
+ }
+
+ private void searchAccount(final Account input, @Nullable final Account output) throws Exception {
+ // Search by id
+ if (output != null) {
+ doSearchAccount(input.getAccountId().toString(), output);
+ }
+
+ // Search by name
+ doSearchAccount(input.getName(), output);
+
+ // Search by email
+ doSearchAccount(input.getEmail(), output);
+
+ // Search by company name
+ doSearchAccount(input.getCompany(), output);
+
+ // Search by external key.
+ // Note: we will always find a match since we don't update it
+ final List<Account> accountsByExternalKey = killBillClient.searchAccounts(input.getExternalKey());
+ Assert.assertEquals(accountsByExternalKey.size(), 1);
+ Assert.assertEquals(accountsByExternalKey.get(0).getAccountId(), input.getAccountId());
+ Assert.assertEquals(accountsByExternalKey.get(0).getExternalKey(), input.getExternalKey());
+ }
+
+ private void doSearchAccount(final String key, @Nullable final Account output) throws Exception {
+ final List<Account> accountsByKey = killBillClient.searchAccounts(key);
+ if (output == null) {
+ Assert.assertEquals(accountsByKey.size(), 0);
+ } else {
+ Assert.assertEquals(accountsByKey.size(), 1);
+ Assert.assertEquals(accountsByKey.get(0), output);
+ }
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestAccountEmail.java b/server/src/test/java/org/killbill/billing/jaxrs/TestAccountEmail.java
new file mode 100644
index 0000000..24a9646
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestAccountEmail.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.AccountEmail;
+
+public class TestAccountEmail extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can create and delete account emails")
+ public void testAddAndRemoveAccountEmail() throws Exception {
+ final Account input = createAccount();
+ final UUID accountId = input.getAccountId();
+
+ final String email1 = UUID.randomUUID().toString();
+ final String email2 = UUID.randomUUID().toString();
+ final AccountEmail accountEmailJson1 = new AccountEmail(accountId, email1);
+ final AccountEmail accountEmailJson2 = new AccountEmail(accountId, email2);
+
+ // Verify the initial state
+ final List<AccountEmail> firstEmails = killBillClient.getEmailsForAccount(accountId);
+ Assert.assertEquals(firstEmails.size(), 0);
+
+ // Add an email
+ killBillClient.addEmailToAccount(accountEmailJson1, createdBy, reason, comment);
+
+ // Verify we can retrieve it
+ final List<AccountEmail> secondEmails = killBillClient.getEmailsForAccount(accountId);
+ Assert.assertEquals(secondEmails.size(), 1);
+ Assert.assertEquals(secondEmails.get(0).getAccountId(), accountId);
+ Assert.assertEquals(secondEmails.get(0).getEmail(), email1);
+
+ // Add another email
+ killBillClient.addEmailToAccount(accountEmailJson2, createdBy, reason, comment);
+
+ // Verify we can retrieve both
+ final List<AccountEmail> thirdEmails = killBillClient.getEmailsForAccount(accountId);
+ Assert.assertEquals(thirdEmails.size(), 2);
+ Assert.assertEquals(thirdEmails.get(0).getAccountId(), accountId);
+ Assert.assertEquals(thirdEmails.get(1).getAccountId(), accountId);
+ Assert.assertTrue(thirdEmails.get(0).getEmail().equals(email1) || thirdEmails.get(0).getEmail().equals(email2));
+ Assert.assertTrue(thirdEmails.get(1).getEmail().equals(email1) || thirdEmails.get(1).getEmail().equals(email2));
+
+ // Delete the first email
+ killBillClient.removeEmailFromAccount(accountEmailJson1, createdBy, reason, comment);
+
+ // Verify it has been deleted
+ final List<AccountEmail> fourthEmails = killBillClient.getEmailsForAccount(accountId);
+ Assert.assertEquals(fourthEmails.size(), 1);
+ Assert.assertEquals(fourthEmails.get(0).getAccountId(), accountId);
+ Assert.assertEquals(fourthEmails.get(0).getEmail(), email2);
+
+ // Try to add the same email
+ killBillClient.addEmailToAccount(accountEmailJson2, createdBy, reason, comment);
+ Assert.assertEquals(killBillClient.getEmailsForAccount(accountId), fourthEmails);
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestAccountEmailNotifications.java b/server/src/test/java/org/killbill/billing/jaxrs/TestAccountEmailNotifications.java
new file mode 100644
index 0000000..0136dd9
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestAccountEmailNotifications.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.InvoiceEmail;
+
+public class TestAccountEmailNotifications extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can toggle email notifications")
+ public void testSetAndUnsetEmailNotifications() throws Exception {
+ final Account input = createAccount();
+ final UUID accountId = input.getAccountId();
+
+ final InvoiceEmail invoiceEmailJsonWithNotifications = new InvoiceEmail(accountId, true);
+ final InvoiceEmail invoiceEmailJsonWithoutNotifications = new InvoiceEmail(accountId, false);
+
+ // Verify the initial state
+ final InvoiceEmail firstInvoiceEmailJson = killBillClient.getEmailNotificationsForAccount(accountId);
+ Assert.assertEquals(firstInvoiceEmailJson.getAccountId(), accountId);
+ Assert.assertFalse(firstInvoiceEmailJson.isNotifiedForInvoices());
+
+ // Enable email notifications
+ killBillClient.updateEmailNotificationsForAccount(invoiceEmailJsonWithNotifications, createdBy, reason, comment);
+
+ // Verify we can retrieve it
+ final InvoiceEmail secondInvoiceEmailJson = killBillClient.getEmailNotificationsForAccount(accountId);
+ Assert.assertEquals(secondInvoiceEmailJson.getAccountId(), accountId);
+ Assert.assertTrue(secondInvoiceEmailJson.isNotifiedForInvoices());
+
+ // Disable email notifications
+ killBillClient.updateEmailNotificationsForAccount(invoiceEmailJsonWithoutNotifications, createdBy, reason, comment);
+
+ // Verify we can retrieve it
+ final InvoiceEmail thirdInvoiceEmailJson = killBillClient.getEmailNotificationsForAccount(accountId);
+ Assert.assertEquals(thirdInvoiceEmailJson.getAccountId(), accountId);
+ Assert.assertFalse(thirdInvoiceEmailJson.isNotifiedForInvoices());
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestAccountTimeline.java b/server/src/test/java/org/killbill/billing/jaxrs/TestAccountTimeline.java
new file mode 100644
index 0000000..435ffc0
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestAccountTimeline.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.AccountTimeline;
+import org.killbill.billing.client.model.AuditLog;
+import org.killbill.billing.client.model.Chargeback;
+import org.killbill.billing.client.model.Credit;
+import org.killbill.billing.client.model.EventSubscription;
+import org.killbill.billing.client.model.Invoice;
+import org.killbill.billing.client.model.Payment;
+import org.killbill.billing.client.model.Refund;
+import org.killbill.billing.util.api.AuditLevel;
+import org.killbill.billing.util.audit.ChangeType;
+
+public class TestAccountTimeline extends TestJaxrsBase {
+
+ private static final String PAYMENT_REQUEST_PROCESSOR = "PaymentRequestProcessor";
+ private static final String TRANSITION = "SubscriptionBaseTransition";
+
+ @Test(groups = "slow", description = "Can retrieve the timeline without audits")
+ public void testAccountTimeline() throws Exception {
+ clock.setTime(new DateTime(2012, 4, 25, 0, 3, 42, 0));
+
+ final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ final AccountTimeline timeline = killBillClient.getAccountTimeline(accountJson.getAccountId());
+ Assert.assertEquals(timeline.getPayments().size(), 1);
+ Assert.assertEquals(timeline.getInvoices().size(), 2);
+ Assert.assertEquals(timeline.getBundles().size(), 1);
+ Assert.assertEquals(timeline.getBundles().get(0).getSubscriptions().size(), 1);
+ Assert.assertEquals(timeline.getBundles().get(0).getSubscriptions().get(0).getEvents().size(), 3);
+ final List<EventSubscription> events = timeline.getBundles().get(0).getSubscriptions().get(0).getEvents();
+ Assert.assertEquals(events.get(0).getEffectiveDate(), new LocalDate(2012, 4, 25));
+ Assert.assertEquals(events.get(0).getEventType(), "START_ENTITLEMENT");
+ Assert.assertEquals(events.get(1).getEffectiveDate(), new LocalDate(2012, 4, 25));
+ Assert.assertEquals(events.get(1).getEventType(), "START_BILLING");
+ Assert.assertEquals(events.get(2).getEffectiveDate(), new LocalDate(2012, 5, 25));
+ Assert.assertEquals(events.get(2).getEventType(), "PHASE");
+ }
+
+ @Test(groups = "slow", description = "Can retrieve the timeline with audits")
+ public void testAccountTimelineWithAudits() throws Exception {
+ final DateTime startTime = clock.getUTCNow();
+ final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+ final DateTime endTime = clock.getUTCNow();
+
+ // Add credit
+ final Invoice invoice = killBillClient.getInvoicesForAccount(accountJson.getAccountId()).get(1);
+ final BigDecimal creditAmount = BigDecimal.ONE;
+ final Credit credit = new Credit();
+ credit.setAccountId(accountJson.getAccountId());
+ credit.setInvoiceId(invoice.getInvoiceId());
+ credit.setCreditAmount(creditAmount);
+ killBillClient.createCredit(credit, createdBy, reason, comment);
+
+ // Add refund
+ final Payment postedPayment = killBillClient.getPaymentsForAccount(accountJson.getAccountId()).get(0);
+ final BigDecimal refundAmount = BigDecimal.ONE;
+ final Refund refund = new Refund();
+ refund.setPaymentId(postedPayment.getPaymentId());
+ refund.setAmount(refundAmount);
+ killBillClient.createRefund(refund, createdBy, reason, comment);
+
+ // Add chargeback
+ final BigDecimal chargebackAmount = BigDecimal.ONE;
+ final Chargeback chargeback = new Chargeback();
+ chargeback.setPaymentId(postedPayment.getPaymentId());
+ chargeback.setAmount(chargebackAmount);
+ killBillClient.createChargeBack(chargeback, createdBy, reason, comment);
+
+ // Verify payments
+ verifyPayments(accountJson.getAccountId(), startTime, endTime, refundAmount, chargebackAmount);
+
+ // Verify invoices
+ verifyInvoices(accountJson.getAccountId(), startTime, endTime);
+
+ // Verify credits
+ verifyCredits(accountJson.getAccountId(), startTime, endTime, creditAmount);
+
+ // Verify bundles
+ verifyBundles(accountJson.getAccountId(), startTime, endTime);
+ }
+
+ private void verifyPayments(final UUID accountId, final DateTime startTime, final DateTime endTime,
+ final BigDecimal refundAmount, final BigDecimal chargebackAmount) throws Exception {
+ for (final AuditLevel auditLevel : AuditLevel.values()) {
+ final AccountTimeline timeline = killBillClient.getAccountTimeline(accountId, auditLevel);
+
+ // Verify payments
+ Assert.assertEquals(timeline.getPayments().size(), 1);
+ final Payment paymentJson = timeline.getPayments().get(0);
+
+ // Verify refunds
+ Assert.assertEquals(paymentJson.getRefunds().size(), 1);
+ final Refund refundJson = paymentJson.getRefunds().get(0);
+ Assert.assertEquals(refundJson.getPaymentId(), paymentJson.getPaymentId());
+ Assert.assertEquals(refundJson.getAmount().compareTo(refundAmount), 0);
+
+ // Verify chargebacks
+ Assert.assertEquals(paymentJson.getChargebacks().size(), 1);
+ final Chargeback chargebackJson = paymentJson.getChargebacks().get(0);
+ Assert.assertEquals(chargebackJson.getPaymentId(), paymentJson.getPaymentId());
+ Assert.assertEquals(chargebackJson.getAmount().compareTo(chargebackAmount), 0);
+
+ // Verify audits
+ final List<AuditLog> paymentAuditLogs = paymentJson.getAuditLogs();
+ final List<AuditLog> refundAuditLogs = refundJson.getAuditLogs();
+ final List<AuditLog> chargebackAuditLogs = chargebackJson.getAuditLogs();
+ if (AuditLevel.NONE.equals(auditLevel)) {
+ // Audits for payments
+ Assert.assertEquals(paymentAuditLogs.size(), 0);
+
+ // Audits for refunds
+ Assert.assertEquals(refundAuditLogs.size(), 0);
+
+ // Audits for chargebacks
+ Assert.assertEquals(chargebackAuditLogs.size(), 0);
+ } else if (AuditLevel.MINIMAL.equals(auditLevel)) {
+ // Audits for payments
+ Assert.assertEquals(paymentAuditLogs.size(), 1);
+ verifyAuditLog(paymentAuditLogs.get(0), ChangeType.INSERT, null, null, PAYMENT_REQUEST_PROCESSOR, startTime, endTime);
+
+ // Audits for refunds
+ Assert.assertEquals(refundAuditLogs.size(), 1);
+ verifyAuditLog(refundAuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+
+ // Audits for chargebacks
+ Assert.assertEquals(chargebackAuditLogs.size(), 1);
+ verifyAuditLog(chargebackAuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+ } else {
+ // Audits for payments
+ Assert.assertEquals(paymentAuditLogs.size(), 2);
+ verifyAuditLog(paymentAuditLogs.get(0), ChangeType.INSERT, null, null, PAYMENT_REQUEST_PROCESSOR, startTime, endTime);
+ verifyAuditLog(paymentAuditLogs.get(1), ChangeType.UPDATE, null, null, PAYMENT_REQUEST_PROCESSOR, startTime, endTime);
+
+ // Audits for refunds
+ Assert.assertEquals(refundAuditLogs.size(), 3);
+ verifyAuditLog(refundAuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+ verifyAuditLog(refundAuditLogs.get(1), ChangeType.UPDATE, reason, comment, createdBy, startTime, endTime);
+ verifyAuditLog(refundAuditLogs.get(2), ChangeType.UPDATE, reason, comment, createdBy, startTime, endTime);
+
+ // Audits for chargebacks
+ Assert.assertEquals(chargebackAuditLogs.size(), 1);
+ verifyAuditLog(chargebackAuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+ }
+ }
+ }
+
+ private void verifyInvoices(final UUID accountId, final DateTime startTime, final DateTime endTime) throws Exception {
+ for (final AuditLevel auditLevel : AuditLevel.values()) {
+ final AccountTimeline timeline = killBillClient.getAccountTimeline(accountId, auditLevel);
+
+ // Verify invoices
+ Assert.assertEquals(timeline.getInvoices().size(), 2);
+
+ // Verify audits
+ final List<AuditLog> firstInvoiceAuditLogs = timeline.getInvoices().get(0).getAuditLogs();
+ final List<AuditLog> secondInvoiceAuditLogs = timeline.getInvoices().get(1).getAuditLogs();
+ if (AuditLevel.NONE.equals(auditLevel)) {
+ Assert.assertEquals(firstInvoiceAuditLogs.size(), 0);
+ Assert.assertEquals(secondInvoiceAuditLogs.size(), 0);
+ } else {
+ Assert.assertEquals(firstInvoiceAuditLogs.size(), 1);
+ verifyAuditLog(firstInvoiceAuditLogs.get(0), ChangeType.INSERT, null, null, TRANSITION, startTime, endTime);
+ Assert.assertEquals(secondInvoiceAuditLogs.size(), 1);
+ verifyAuditLog(secondInvoiceAuditLogs.get(0), ChangeType.INSERT, null, null, TRANSITION, startTime, endTime);
+ }
+ }
+ }
+
+ private void verifyCredits(final UUID accountId, final DateTime startTime, final DateTime endTime, final BigDecimal creditAmount) throws Exception {
+ for (final AuditLevel auditLevel : AuditLevel.values()) {
+ final AccountTimeline timeline = killBillClient.getAccountTimeline(accountId, auditLevel);
+
+ // Verify credits
+ final List<Credit> credits = timeline.getInvoices().get(1).getCredits();
+ Assert.assertEquals(credits.size(), 1);
+ Assert.assertEquals(credits.get(0).getCreditAmount().compareTo(creditAmount.negate()), 0);
+
+ // Verify audits
+ final List<AuditLog> creditAuditLogs = credits.get(0).getAuditLogs();
+ if (AuditLevel.NONE.equals(auditLevel)) {
+ Assert.assertEquals(creditAuditLogs.size(), 0);
+ } else {
+ Assert.assertEquals(creditAuditLogs.size(), 1);
+ verifyAuditLog(creditAuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+ }
+ }
+ }
+
+ private void verifyBundles(final UUID accountId, final DateTime startTime, final DateTime endTime) throws Exception {
+ for (final AuditLevel auditLevel : AuditLevel.values()) {
+ final AccountTimeline timeline = killBillClient.getAccountTimeline(accountId, auditLevel);
+
+ // Verify bundles
+ Assert.assertEquals(timeline.getBundles().size(), 1);
+ Assert.assertEquals(timeline.getBundles().get(0).getSubscriptions().size(), 1);
+ Assert.assertEquals(timeline.getBundles().get(0).getSubscriptions().get(0).getEvents().size(), 3);
+
+ // Verify audits
+ final List<AuditLog> bundleAuditLogs = timeline.getBundles().get(0).getAuditLogs();
+ final List<AuditLog> subscriptionAuditLogs = timeline.getBundles().get(0).getSubscriptions().get(0).getAuditLogs();
+ final List<AuditLog> subscriptionEvent1AuditLogs = timeline.getBundles().get(0).getSubscriptions().get(0).getEvents().get(0).getAuditLogs();
+ final List<AuditLog> subscriptionEvent2AuditLogs = timeline.getBundles().get(0).getSubscriptions().get(0).getEvents().get(1).getAuditLogs();
+ if (AuditLevel.NONE.equals(auditLevel)) {
+ // Audits for bundles
+ Assert.assertEquals(bundleAuditLogs.size(), 0);
+
+ // Audits for subscriptions
+ Assert.assertEquals(subscriptionAuditLogs.size(), 0);
+
+ // Audit for subscription events
+ Assert.assertEquals(subscriptionEvent1AuditLogs.size(), 0);
+ Assert.assertEquals(subscriptionEvent2AuditLogs.size(), 0);
+ } else if (AuditLevel.MINIMAL.equals(auditLevel)) {
+ // Audits for bundles
+ Assert.assertEquals(bundleAuditLogs.size(), 1);
+ verifyAuditLog(bundleAuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+
+ // Audits for subscriptions
+ Assert.assertEquals(subscriptionAuditLogs.size(), 1);
+ verifyAuditLog(subscriptionAuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+
+ // Audit for subscription events
+ Assert.assertEquals(subscriptionEvent1AuditLogs.size(), 1);
+ verifyAuditLog(subscriptionEvent1AuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+ Assert.assertEquals(subscriptionEvent2AuditLogs.size(), 1);
+ verifyAuditLog(subscriptionEvent2AuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+ } else {
+ // Audits for bundles
+ Assert.assertEquals(bundleAuditLogs.size(), 3);
+ verifyAuditLog(bundleAuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+ verifyAuditLog(bundleAuditLogs.get(1), ChangeType.UPDATE, null, null, TRANSITION, startTime, endTime);
+ verifyAuditLog(bundleAuditLogs.get(2), ChangeType.UPDATE, null, null, TRANSITION, startTime, endTime);
+
+ // Audits for subscriptions
+ Assert.assertEquals(subscriptionAuditLogs.size(), 3);
+ verifyAuditLog(subscriptionAuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+ verifyAuditLog(subscriptionAuditLogs.get(1), ChangeType.UPDATE, null, null, TRANSITION, startTime, endTime);
+ verifyAuditLog(subscriptionAuditLogs.get(2), ChangeType.UPDATE, null, null, TRANSITION, startTime, endTime);
+
+ // Audit for subscription events
+ Assert.assertEquals(subscriptionEvent1AuditLogs.size(), 1);
+ verifyAuditLog(subscriptionEvent1AuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+ Assert.assertEquals(subscriptionEvent2AuditLogs.size(), 1);
+ verifyAuditLog(subscriptionEvent2AuditLogs.get(0), ChangeType.INSERT, reason, comment, createdBy, startTime, endTime);
+ }
+ }
+ }
+
+ private void verifyAuditLog(final AuditLog auditLogJson, final ChangeType changeType, @Nullable final String reasonCode,
+ @Nullable final String comments, @Nullable final String changedBy,
+ final DateTime startTime, final DateTime endTime) {
+ Assert.assertEquals(auditLogJson.getChangeType(), changeType.toString());
+ Assert.assertFalse(auditLogJson.getChangeDate().isBefore(startTime));
+ // Flaky
+ //Assert.assertFalse(auditLogJson.getChangeDate().isAfter(endTime));
+ Assert.assertEquals(auditLogJson.getReasonCode(), reasonCode);
+ Assert.assertEquals(auditLogJson.getComments(), comments);
+ Assert.assertEquals(auditLogJson.getChangedBy(), changedBy);
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestBundle.java b/server/src/test/java/org/killbill/billing/jaxrs/TestBundle.java
new file mode 100644
index 0000000..b6d308b
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestBundle.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Bundle;
+import org.killbill.billing.client.model.Bundles;
+import org.killbill.billing.client.model.Subscription;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotEquals;
+
+public class TestBundle extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can retrieve bundles by external key")
+ public void testBundleOk() throws Exception {
+ final Account accountJson = createAccount();
+
+ createEntitlement(accountJson.getAccountId(), "123467", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, true);
+
+ // Retrieves by external key
+ final List<Bundle> objFromJson = killBillClient.getAccountBundles(accountJson.getAccountId(), "123467");
+ Assert.assertEquals(objFromJson.size(), 1);
+ }
+
+ @Test(groups = "slow", description = "Can retrieve account bundles")
+ public void testBundleFromAccount() throws Exception {
+ final Account accountJson = createAccount();
+ createEntitlement(accountJson.getAccountId(), "156567", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, true);
+ createEntitlement(accountJson.getAccountId(), "265658", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, true);
+
+ final List<Bundle> objFromJson = killBillClient.getAccountBundles(accountJson.getAccountId());
+ Assert.assertEquals(objFromJson.size(), 2);
+ }
+
+ @Test(groups = "slow", description = "Can handle non existent bundle")
+ public void testBundleNonExistent() throws Exception {
+ final Account accountJson = createAccount();
+
+ Assert.assertNull(killBillClient.getBundle(UUID.randomUUID()));
+ Assert.assertTrue(killBillClient.getAccountBundles(accountJson.getAccountId(), "98374982743892").isEmpty());
+ Assert.assertTrue(killBillClient.getAccountBundles(accountJson.getAccountId()).isEmpty());
+ }
+
+ @Test(groups = "slow", description = "Can handle non existent account")
+ public void testAccountNonExistent() throws Exception {
+ Assert.assertTrue(killBillClient.getAccountBundles(UUID.randomUUID()).isEmpty());
+ }
+
+ @Test(groups = "slow", description = "Can transfer bundle")
+ public void testBundleTransfer() throws Exception {
+ final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String bundleExternalKey = "93199";
+
+ final Subscription entitlementJsonNoEvents = createEntitlement(accountJson.getAccountId(), bundleExternalKey, productName,
+ ProductCategory.BASE, term, true);
+
+ final Bundle originalBundle = killBillClient.getBundle(bundleExternalKey);
+ assertEquals(originalBundle.getAccountId(), accountJson.getAccountId());
+ assertEquals(originalBundle.getExternalKey(), bundleExternalKey);
+
+ final Account newAccount = createAccountWithDefaultPaymentMethod();
+
+ final Bundle bundle = new Bundle();
+ bundle.setAccountId(newAccount.getAccountId());
+ bundle.setBundleId(entitlementJsonNoEvents.getBundleId());
+ assertEquals(killBillClient.transferBundle(bundle, createdBy, reason, comment).getAccountId(), newAccount.getAccountId());
+
+ final Bundle newBundle = killBillClient.getBundle(bundleExternalKey);
+ assertNotEquals(newBundle.getBundleId(), originalBundle.getBundleId());
+ assertEquals(newBundle.getExternalKey(), originalBundle.getExternalKey());
+ assertEquals(newBundle.getAccountId(), newAccount.getAccountId());
+ }
+
+ @Test(groups = "slow", description = "Can paginate and search through all bundles")
+ public void testBundlesPagination() throws Exception {
+ final Account accountJson = createAccount();
+
+ for (int i = 0; i < 5; i++) {
+ createEntitlement(accountJson.getAccountId(), UUID.randomUUID().toString(), "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, true);
+ }
+
+ final Bundles allBundles = killBillClient.getBundles();
+ Assert.assertEquals(allBundles.size(), 5);
+
+ for (final Bundle bundle : allBundles) {
+ Assert.assertEquals(killBillClient.searchBundles(bundle.getBundleId().toString()).size(), 1);
+ Assert.assertEquals(killBillClient.searchBundles(bundle.getAccountId().toString()).size(), 5);
+ Assert.assertEquals(killBillClient.searchBundles(bundle.getExternalKey()).size(), 1);
+ }
+
+ Bundles page = killBillClient.getBundles(0L, 1L);
+ for (int i = 0; i < 5; i++) {
+ Assert.assertNotNull(page);
+ Assert.assertEquals(page.size(), 1);
+ Assert.assertEquals(page.get(0), allBundles.get(i));
+ page = page.getNext();
+ }
+ Assert.assertNull(page);
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java b/server/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
new file mode 100644
index 0000000..2262b48
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.model.Catalog;
+import org.killbill.billing.client.model.Plan;
+import org.killbill.billing.client.model.PlanDetail;
+import org.killbill.billing.client.model.Product;
+
+public class TestCatalog extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can retrieve a simplified version of the catalog")
+ public void testCatalogSimple() throws Exception {
+ final Set<String> allBasePlans = new HashSet<String>();
+
+ final Catalog catalogJsonSimple = killBillClient.getSimpleCatalog();
+ for (final Product productJson : catalogJsonSimple.getProducts()) {
+ if (!"BASE".equals(productJson.getType())) {
+ Assert.assertEquals(productJson.getIncluded().size(), 0);
+ Assert.assertEquals(productJson.getAvailable().size(), 0);
+ continue;
+ }
+
+ // Save all plans for later (see below)
+ for (final Plan planJson : productJson.getPlans()) {
+ allBasePlans.add(planJson.getName());
+ }
+
+ // Retrieve available products (addons) for that base product
+ final List<PlanDetail> availableAddons = killBillClient.getAvailableAddons(productJson.getName());
+ final Set<String> availableAddonsNames = new HashSet<String>();
+ for (final PlanDetail planDetailJson : availableAddons) {
+ availableAddonsNames.add(planDetailJson.getProductName());
+ }
+ Assert.assertEquals(availableAddonsNames, new HashSet<String>(productJson.getAvailable()));
+ }
+
+ // Verify base plans endpoint
+ final List<PlanDetail> basePlans = killBillClient.getBasePlans();
+ final Set<String> foundBasePlans = new HashSet<String>();
+ for (final PlanDetail planDetailJson : basePlans) {
+ foundBasePlans.add(planDetailJson.getPlanName());
+ }
+ Assert.assertEquals(foundBasePlans, allBasePlans);
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java b/server/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java
new file mode 100644
index 0000000..b6d60bc
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestChargeback.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Chargeback;
+import org.killbill.billing.client.model.Invoice;
+import org.killbill.billing.client.model.Payment;
+import org.killbill.billing.client.model.Subscription;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class TestChargeback extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can create a chargeback")
+ public void testAddChargeback() throws Exception {
+ final Payment payment = createAccountWithInvoiceAndPayment();
+ createAndVerifyChargeback(payment);
+ }
+
+ @Test(groups = "slow", description = "Can create multiple chargebacks")
+ public void testMultipleChargeback() throws Exception {
+ final Payment payment = createAccountWithInvoiceAndPayment();
+
+ // We get a 249.95 payment so we do 4 chargeback and then the fifth should fail
+ final Chargeback input = new Chargeback();
+ input.setAmount(new BigDecimal("50.00"));
+ input.setPaymentId(payment.getPaymentId());
+
+ int count = 4;
+ while (count-- > 0) {
+ assertNotNull(killBillClient.createChargeBack(input, createdBy, reason, comment));
+ }
+
+ // Last attempt should fail because this is more than the Payment
+ try {
+ killBillClient.createChargeBack(input, createdBy, reason, comment);
+ fail();
+ } catch (final KillBillClientException e) {
+ }
+
+ // Find the chargeback by account
+ List<Chargeback> chargebacks = killBillClient.getChargebacksForAccount(payment.getAccountId());
+ assertEquals(chargebacks.size(), 4);
+ for (final Chargeback chargeBack : chargebacks) {
+ assertTrue(chargeBack.getAmount().compareTo(input.getAmount()) == 0);
+ assertEquals(chargeBack.getPaymentId(), input.getPaymentId());
+ }
+
+ // Find the chargeback by payment
+ chargebacks = killBillClient.getChargebacksForPayment(payment.getPaymentId());
+ assertEquals(chargebacks.size(), 4);
+ }
+
+ @Test(groups = "slow", description = "Can add a chargeback for deleted payment methods")
+ public void testAddChargebackForDeletedPaymentMethod() throws Exception {
+ final Payment payment = createAccountWithInvoiceAndPayment();
+
+ // Check the payment method exists
+ assertEquals(killBillClient.getAccount(payment.getAccountId()).getPaymentMethodId(), payment.getPaymentMethodId());
+ assertEquals(killBillClient.getPaymentMethod(payment.getPaymentMethodId()).getAccountId(), payment.getAccountId());
+
+ // Delete the payment method
+ killBillClient.deletePaymentMethod(payment.getPaymentMethodId(), true, createdBy, reason, comment);
+
+ // Check the payment method was deleted
+ assertNull(killBillClient.getAccount(payment.getAccountId()).getPaymentMethodId());
+
+ createAndVerifyChargeback(payment);
+ }
+
+ @Test(groups = "slow", description = "Cannot add a chargeback for non existent payment")
+ public void testInvoicePaymentDoesNotExist() throws Exception {
+ final Chargeback input = new Chargeback();
+ input.setAmount(BigDecimal.TEN);
+ input.setPaymentId(UUID.randomUUID());
+ assertNull(killBillClient.createChargeBack(input, createdBy, reason, comment));
+ }
+
+ @Test(groups = "slow", description = "Cannot add a badly formatted chargeback")
+ public void testBadRequest() throws Exception {
+ final Payment payment = createAccountWithInvoiceAndPayment();
+
+ final Chargeback input = new Chargeback();
+ input.setAmount(BigDecimal.TEN.negate());
+ input.setPaymentId(payment.getPaymentId());
+
+ try {
+ killBillClient.createChargeBack(input, createdBy, reason, comment);
+ fail();
+ } catch (final KillBillClientException e) {
+ }
+ }
+
+ @Test(groups = "slow", description = "Accounts can have zero chargeback")
+ public void testNoChargebackForAccount() throws Exception {
+ Assert.assertEquals(killBillClient.getChargebacksForAccount(UUID.randomUUID()).size(), 0);
+ }
+
+ @Test(groups = "slow", description = "Payments can have zero chargeback")
+ public void testNoChargebackForPayment() throws Exception {
+ Assert.assertEquals(killBillClient.getChargebacksForPayment(UUID.randomUUID()).size(), 0);
+ }
+
+ private void createAndVerifyChargeback(final Payment payment) throws KillBillClientException {
+ // Create the chargeback
+ final Chargeback chargeback = new Chargeback();
+ chargeback.setPaymentId(payment.getPaymentId());
+ chargeback.setAmount(BigDecimal.TEN);
+ final Chargeback chargebackJson = killBillClient.createChargeBack(chargeback, createdBy, reason, comment);
+ assertEquals(chargebackJson.getAmount().compareTo(chargeback.getAmount()), 0);
+ assertEquals(chargebackJson.getPaymentId(), chargeback.getPaymentId());
+
+ // Find the chargeback by account
+ List<Chargeback> chargebacks = killBillClient.getChargebacksForAccount(payment.getAccountId());
+ assertEquals(chargebacks.size(), 1);
+ assertEquals(chargebacks.get(0).getAmount().compareTo(chargeback.getAmount()), 0);
+ assertEquals(chargebacks.get(0).getPaymentId(), chargeback.getPaymentId());
+
+ // Find the chargeback by payment
+ chargebacks = killBillClient.getChargebacksForPayment(payment.getPaymentId());
+ assertEquals(chargebacks.size(), 1);
+ assertEquals(chargebacks.get(0).getAmount().compareTo(chargeback.getAmount()), 0);
+ assertEquals(chargebacks.get(0).getPaymentId(), chargeback.getPaymentId());
+ }
+
+ private Payment createAccountWithInvoiceAndPayment() throws Exception {
+ final Invoice invoice = createAccountWithInvoice();
+ return getPayment(invoice);
+ }
+
+ private Invoice createAccountWithInvoice() throws Exception {
+ // Create account
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+
+ // Create subscription
+ final Subscription subscriptionJson = createEntitlement(accountJson.getAccountId(), "6253283", "Shotgun",
+ ProductCategory.BASE, BillingPeriod.MONTHLY, true);
+ assertNotNull(subscriptionJson);
+
+ // Move after the trial period to trigger an invoice with a non-zero invoice item
+ clock.addDays(32);
+ crappyWaitForLackOfProperSynchonization();
+
+ // Retrieve the invoice
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ // We should have two invoices, one for the trial (zero dollar amount) and one for the first month
+ assertEquals(invoices.size(), 2);
+ assertTrue(invoices.get(1).getAmount().doubleValue() > 0);
+
+ return invoices.get(1);
+ }
+
+ private Payment getPayment(final Invoice invoice) throws KillBillClientException {
+ final List<Payment> payments = killBillClient.getPaymentsForInvoice(invoice.getInvoiceId());
+ assertNotNull(payments);
+ assertEquals(payments.size(), 1);
+ return payments.get(0);
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestCredit.java b/server/src/test/java/org/killbill/billing/jaxrs/TestCredit.java
new file mode 100644
index 0000000..77628aa
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestCredit.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Credit;
+import org.killbill.billing.client.model.Invoice;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.fail;
+
+public class TestCredit extends TestJaxrsBase {
+
+ Account accountJson;
+
+ @BeforeMethod(groups = "slow")
+ public void setUp() throws Exception {
+ accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+ }
+
+ @Test(groups = "slow", description = "Can add a credit to an existing invoice")
+ public void testAddCreditToInvoice() throws Exception {
+ final Invoice invoice = killBillClient.getInvoicesForAccount(accountJson.getAccountId()).get(1);
+
+ final DateTime effectiveDate = clock.getUTCNow();
+ final BigDecimal creditAmount = BigDecimal.ONE;
+ final Credit credit = new Credit();
+ credit.setAccountId(accountJson.getAccountId());
+ credit.setInvoiceId(invoice.getInvoiceId());
+ credit.setCreditAmount(creditAmount);
+ final Credit objFromJson = killBillClient.createCredit(credit, createdBy, reason, comment);
+
+ // We can't just compare the object via .equals() due e.g. to the invoice id
+ assertEquals(objFromJson.getAccountId(), accountJson.getAccountId());
+ assertEquals(objFromJson.getInvoiceId(), invoice.getInvoiceId());
+ assertEquals(objFromJson.getCreditAmount().compareTo(creditAmount), 0);
+ assertEquals(objFromJson.getEffectiveDate().compareTo(effectiveDate.toLocalDate()), 0);
+ }
+
+ @Test(groups = "slow", description = "Cannot add a credit if the account doesn't exist")
+ public void testAccountDoesNotExist() throws Exception {
+ final Credit credit = new Credit();
+ credit.setAccountId(UUID.randomUUID());
+ credit.setCreditAmount(BigDecimal.TEN);
+
+ // Try to create the credit
+ assertNull(killBillClient.createCredit(credit, createdBy, reason, comment));
+ }
+
+ @Test(groups = "slow", description = "Cannot credit a badly formatted credit")
+ public void testBadRequest() throws Exception {
+ final Credit credit = new Credit();
+ credit.setAccountId(accountJson.getAccountId());
+ credit.setCreditAmount(BigDecimal.TEN.negate());
+
+ // Try to create the credit
+ try {
+ killBillClient.createCredit(credit, createdBy, reason, comment);
+ fail();
+ } catch (final KillBillClientException e) {
+ }
+ }
+
+ @Test(groups = "slow", description = "Cannot retrieve a non existing credit")
+ public void testCreditDoesNotExist() throws Exception {
+ assertNull(killBillClient.getCredit(UUID.randomUUID()));
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java b/server/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java
new file mode 100644
index 0000000..c26a30d
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+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;
+
+public class TestCustomField extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can paginate through all custom fields")
+ public void testCustomFieldsPagination() throws Exception {
+ final Account account = createAccount();
+ for (int i = 0; i < 5; i++) {
+ 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);
+ }
+
+ final CustomFields allCustomFields = killBillClient.getCustomFields();
+ Assert.assertEquals(allCustomFields.size(), 5);
+
+ CustomFields page = killBillClient.getCustomFields(0L, 1L);
+ for (int i = 0; i < 5; i++) {
+ Assert.assertNotNull(page);
+ Assert.assertEquals(page.size(), 1);
+ Assert.assertEquals(page.get(0), allCustomFields.get(i));
+ page = page.getNext();
+ }
+ Assert.assertNull(page);
+
+ for (final CustomField customField : allCustomFields) {
+ doSearchCustomField(UUID.randomUUID().toString(), null);
+ doSearchCustomField(customField.getName(), customField);
+ doSearchCustomField(customField.getValue(), customField);
+ }
+
+ final CustomFields customFields = killBillClient.searchCustomFields(ObjectType.ACCOUNT.toString());
+ Assert.assertEquals(customFields.size(), 5);
+ Assert.assertEquals(customFields.getPaginationCurrentOffset(), 0);
+ Assert.assertEquals(customFields.getPaginationTotalNbRecords(), 5);
+ Assert.assertEquals(customFields.getPaginationMaxNbRecords(), 5);
+ }
+
+ private void doSearchCustomField(final String searchKey, @Nullable final CustomField expectedCustomField) throws KillBillClientException {
+ final CustomFields customFields = killBillClient.searchCustomFields(searchKey);
+ if (expectedCustomField == null) {
+ Assert.assertTrue(customFields.isEmpty());
+ Assert.assertEquals(customFields.getPaginationCurrentOffset(), 0);
+ Assert.assertEquals(customFields.getPaginationTotalNbRecords(), 0);
+ Assert.assertEquals(customFields.getPaginationMaxNbRecords(), 5);
+ } else {
+ Assert.assertEquals(customFields.size(), 1);
+ Assert.assertEquals(customFields.get(0), expectedCustomField);
+ Assert.assertEquals(customFields.getPaginationCurrentOffset(), 0);
+ Assert.assertEquals(customFields.getPaginationTotalNbRecords(), 1);
+ Assert.assertEquals(customFields.getPaginationMaxNbRecords(), 5);
+ }
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java b/server/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
new file mode 100644
index 0000000..f960cd0
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Subscription;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestEntitlement extends TestJaxrsBase {
+
+ private static final int CALL_COMPLETION_TIMEOUT_SEC = 5;
+
+ @Test(groups = "slow", description = "Can change plan and cancel a subscription")
+ public void testEntitlementInTrialOk() throws Exception {
+ final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+
+ final Subscription entitlementJson = createEntitlement(accountJson.getAccountId(), "99999", productName,
+ ProductCategory.BASE, term, true);
+
+ // Retrieves with GET
+ Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ Assert.assertTrue(objFromJson.equals(entitlementJson));
+
+ // Change plan IMM
+ final String newProductName = "Assault-Rifle";
+
+ final Subscription newInput = new Subscription();
+ newInput.setSubscriptionId(entitlementJson.getSubscriptionId());
+ newInput.setProductName(newProductName);
+ newInput.setBillingPeriod(entitlementJson.getBillingPeriod());
+ newInput.setPriceList(entitlementJson.getPriceList());
+ objFromJson = killBillClient.updateSubscription(newInput, CALL_COMPLETION_TIMEOUT_SEC, createdBy, reason, comment);
+ Assert.assertNotNull(objFromJson);
+
+ // MOVE AFTER TRIAL
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ crappyWaitForLackOfProperSynchonization();
+
+ // Cancel IMM (Billing EOT)
+ killBillClient.cancelSubscription(newInput.getSubscriptionId(), CALL_COMPLETION_TIMEOUT_SEC, createdBy, reason, comment);
+
+ // Retrieves to check EndDate
+ objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ assertNotNull(objFromJson.getCancelledDate());
+ assertTrue(objFromJson.getCancelledDate().compareTo(new LocalDate(clock.getUTCNow())) == 0);
+ }
+
+ @Test(groups = "slow", description = "Can cancel and uncancel a subscription")
+ public void testEntitlementUncancel() throws Exception {
+ final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+
+ final Subscription entitlementJson = createEntitlement(accountJson.getAccountId(), "99999", productName,
+ ProductCategory.BASE, term, true);
+
+ // Retrieves with GET
+ Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ Assert.assertTrue(objFromJson.equals(entitlementJson));
+
+ // MOVE AFTER TRIAL
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ crappyWaitForLackOfProperSynchonization();
+
+ // Cancel EOT
+ killBillClient.cancelSubscription(entitlementJson.getSubscriptionId(), EntitlementActionPolicy.END_OF_TERM,
+ BillingActionPolicy.END_OF_TERM, CALL_COMPLETION_TIMEOUT_SEC, createdBy, reason, comment);
+
+ // Retrieves to check EndDate
+ objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ assertNotNull(objFromJson.getCancelledDate());
+
+ // Uncancel
+ killBillClient.uncancelSubscription(entitlementJson.getSubscriptionId(), createdBy, reason, comment);
+
+ objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ assertNull(objFromJson.getCancelledDate());
+ }
+
+ @Test(groups = "slow", description = "Can handle non existent subscription")
+ public void testWithNonExistentEntitlement() throws Exception {
+ final UUID subscriptionId = UUID.randomUUID();
+ final Subscription subscription = new Subscription();
+ subscription.setSubscriptionId(subscriptionId);
+ subscription.setProductName("Pistol");
+ subscription.setBillingPeriod(BillingPeriod.ANNUAL);
+ subscription.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ Assert.assertNull(killBillClient.updateSubscription(subscription, createdBy, reason, comment));
+
+ // No-op (404, doesn't throw an exception)
+ killBillClient.cancelSubscription(subscriptionId, createdBy, reason, comment);
+
+ Assert.assertNull(killBillClient.getSubscription(subscriptionId));
+ }
+
+ @Test(groups = "slow", description = "Can override billing policy on change")
+ public void testOverridePolicy() throws Exception {
+ final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.ANNUAL;
+
+ final Subscription subscriptionJson = createEntitlement(accountJson.getAccountId(), "99999", productName,
+ ProductCategory.BASE, term, true);
+
+ // Retrieves with GET
+ Subscription objFromJson = killBillClient.getSubscription(subscriptionJson.getSubscriptionId());
+ Assert.assertTrue(objFromJson.equals(subscriptionJson));
+ assertEquals(objFromJson.getBillingPeriod(), BillingPeriod.ANNUAL);
+
+ // Change billing period immediately
+ final Subscription newInput = new Subscription();
+ newInput.setSubscriptionId(subscriptionJson.getSubscriptionId());
+ newInput.setProductName(subscriptionJson.getProductName());
+ newInput.setBillingPeriod(BillingPeriod.MONTHLY);
+ newInput.setPriceList(subscriptionJson.getPriceList());
+ objFromJson = killBillClient.updateSubscription(newInput, BillingActionPolicy.IMMEDIATE, CALL_COMPLETION_TIMEOUT_SEC, createdBy, reason, comment);
+ Assert.assertNotNull(objFromJson);
+ assertEquals(objFromJson.getBillingPeriod(), BillingPeriod.MONTHLY);
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestExceptions.java b/server/src/test/java/org/killbill/billing/jaxrs/TestExceptions.java
new file mode 100644
index 0000000..659d003
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestExceptions.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2013 Ning, Incc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Chargeback;
+import org.killbill.billing.client.model.Payment;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+
+import static org.testng.Assert.fail;
+
+public class TestExceptions extends TestJaxrsBase {
+
+ @Test(groups = "slow")
+ public void testExceptionMapping() throws Exception {
+ final Account account = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+ final List<Payment> payments = killBillClient.getPaymentsForAccount(account.getAccountId());
+ final Chargeback input = new Chargeback();
+ input.setAmount(BigDecimal.TEN.negate());
+ input.setPaymentId(payments.get(0).getPaymentId());
+
+ try {
+ killBillClient.createChargeBack(input, createdBy, reason, comment);
+ fail();
+ } catch (final KillBillClientException e) {
+ Assert.assertEquals(e.getBillingException().getClassName(), InvoiceApiException.class.getName());
+ Assert.assertEquals(e.getBillingException().getCode(), (Integer) ErrorCode.CHARGE_BACK_AMOUNT_IS_NEGATIVE.getCode());
+ Assert.assertFalse(e.getBillingException().getStackTrace().isEmpty());
+ }
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java b/server/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
new file mode 100644
index 0000000..3500434
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.AuditLog;
+import org.killbill.billing.client.model.Invoice;
+import org.killbill.billing.client.model.InvoiceItem;
+import org.killbill.billing.client.model.Invoices;
+import org.killbill.billing.client.model.Payment;
+import org.killbill.billing.client.model.PaymentMethod;
+import org.killbill.billing.payment.provider.ExternalPaymentProviderPlugin;
+import org.killbill.billing.util.api.AuditLevel;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestInvoice extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can search and retrieve invoices with and without items")
+ public void testInvoiceOk() throws Exception {
+ final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, AuditLevel.FULL);
+ assertEquals(invoices.size(), 2);
+ for (final Invoice invoiceJson : invoices) {
+ Assert.assertEquals(invoiceJson.getAuditLogs().size(), 1);
+ final AuditLog auditLogJson = invoiceJson.getAuditLogs().get(0);
+ Assert.assertEquals(auditLogJson.getChangeType(), "INSERT");
+ Assert.assertEquals(auditLogJson.getChangedBy(), "SubscriptionBaseTransition");
+ Assert.assertFalse(auditLogJson.getChangeDate().isBefore(initialDate));
+ Assert.assertNotNull(auditLogJson.getUserToken());
+ Assert.assertNull(auditLogJson.getReasonCode());
+ Assert.assertNull(auditLogJson.getComments());
+ }
+
+ final Invoice invoiceJson = invoices.get(0);
+
+ // Check get with & without items
+ assertTrue(killBillClient.getInvoice(invoiceJson.getInvoiceId(), Boolean.FALSE).getItems().isEmpty());
+ assertTrue(killBillClient.getInvoice(invoiceJson.getInvoiceNumber(), Boolean.FALSE).getItems().isEmpty());
+ assertEquals(killBillClient.getInvoice(invoiceJson.getInvoiceId(), Boolean.TRUE).getItems().size(), invoiceJson.getItems().size());
+ assertEquals(killBillClient.getInvoice(invoiceJson.getInvoiceNumber(), Boolean.TRUE).getItems().size(), invoiceJson.getItems().size());
+
+ // Check we can retrieve an individual invoice
+ final Invoice firstInvoice = killBillClient.getInvoice(invoiceJson.getInvoiceId());
+ assertEquals(firstInvoice, invoiceJson);
+
+ // Check we can retrieve the invoice by number
+ final Invoice firstInvoiceByNumberJson = killBillClient.getInvoice(invoiceJson.getInvoiceNumber());
+ assertEquals(firstInvoiceByNumberJson, invoiceJson);
+
+ // Then create a dryRun Invoice
+ final DateTime futureDate = clock.getUTCNow().plusMonths(1).plusDays(3);
+ killBillClient.createDryRunInvoice(accountJson.getAccountId(), futureDate, createdBy, reason, comment);
+
+ // The one more time with no DryRun
+ killBillClient.createInvoice(accountJson.getAccountId(), futureDate, createdBy, reason, comment);
+
+ // Check again # invoices, should be 3 this time
+ final List<Invoice> newInvoiceList = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ assertEquals(newInvoiceList.size(), 3);
+ }
+
+ @Test(groups = "slow", description = "Can retrieve invoice payments")
+ public void testInvoicePayments() throws Exception {
+ clock.setTime(new DateTime(2012, 4, 25, 0, 3, 42, 0));
+
+ final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ assertEquals(invoices.size(), 2);
+
+ for (final Invoice cur : invoices) {
+ final List<Payment> objFromJson = killBillClient.getPaymentsForInvoice(cur.getInvoiceId());
+
+ if (cur.getAmount().compareTo(BigDecimal.ZERO) == 0) {
+ assertEquals(objFromJson.size(), 0);
+ } else {
+ assertEquals(objFromJson.size(), 1);
+ assertEquals(cur.getAmount().compareTo(objFromJson.get(0).getAmount()), 0);
+ }
+ }
+ }
+
+ @Test(groups = "slow", description = "Can pay invoices")
+ public void testPayAllInvoices() throws Exception {
+ clock.setTime(new DateTime(2012, 4, 25, 0, 3, 42, 0));
+
+ // No payment method
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Check there was no payment made
+ assertEquals(killBillClient.getPaymentsForAccount(accountJson.getAccountId()).size(), 1);
+
+ // Get the invoices
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ assertEquals(invoices.size(), 2);
+ final Invoice invoiceToPay = invoices.get(1);
+ assertEquals(invoiceToPay.getBalance().compareTo(BigDecimal.ZERO), 1);
+
+ // Pay all invoices
+ killBillClient.payAllInvoices(accountJson.getAccountId(), true, createdBy, reason, comment);
+ for (final Invoice invoice : killBillClient.getInvoicesForAccount(accountJson.getAccountId())) {
+ assertEquals(invoice.getBalance().compareTo(BigDecimal.ZERO), 0);
+ }
+ assertEquals(killBillClient.getPaymentsForAccount(accountJson.getAccountId()).size(), 2);
+ }
+
+ @Test(groups = "slow", description = "Can create an insta-payment")
+ public void testInvoiceCreatePayment() throws Exception {
+ clock.setTime(new DateTime(2012, 4, 25, 0, 3, 42, 0));
+
+ // STEPH MISSING SET ACCOUNT AUTO_PAY_OFF
+ final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Get the invoices
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ assertEquals(invoices.size(), 2);
+
+ for (final Invoice cur : invoices) {
+ if (cur.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
+ continue;
+ }
+
+ // CREATE INSTA PAYMENT
+ final Payment payment = new Payment();
+ payment.setAccountId(accountJson.getAccountId());
+ payment.setInvoiceId(cur.getInvoiceId());
+ payment.setAmount(cur.getBalance());
+ final List<Payment> objFromJson = killBillClient.createPayment(payment, false, createdBy, reason, comment);
+ assertEquals(objFromJson.size(), 1);
+ assertEquals(cur.getBalance().compareTo(objFromJson.get(0).getAmount()), 0);
+ }
+ }
+
+ @Test(groups = "slow", description = "Can create an external payment")
+ public void testExternalPayment() throws Exception {
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Verify we didn't get any payment
+ final List<Payment> noPaymentsFromJson = killBillClient.getPaymentsForAccount(accountJson.getAccountId());
+ assertEquals(noPaymentsFromJson.size(), 1);
+ final UUID initialPaymentId = noPaymentsFromJson.get(0).getPaymentId();
+
+ // Get the invoices
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ // 2 invoices but look for the non zero dollar one
+ assertEquals(invoices.size(), 2);
+ final UUID invoiceId = invoices.get(1).getInvoiceId();
+
+ // Post an external payment
+ final BigDecimal paidAmount = BigDecimal.TEN;
+ final Payment payment = new Payment();
+ payment.setAmount(BigDecimal.TEN);
+ payment.setAccountId(accountJson.getAccountId());
+ payment.setInvoiceId(invoiceId);
+ killBillClient.createPayment(payment, true, createdBy, reason, comment);
+
+ // Verify we indeed got the payment
+ final List<Payment> paymentsFromJson = killBillClient.getPaymentsForAccount(accountJson.getAccountId());
+ assertEquals(paymentsFromJson.size(), 2);
+ Payment secondPayment = null;
+ for (final Payment cur : paymentsFromJson) {
+ if (!cur.getPaymentId().equals(initialPaymentId)) {
+ secondPayment = cur;
+ break;
+ }
+ }
+ assertNotNull(secondPayment);
+
+ assertEquals(secondPayment.getPaidAmount().compareTo(paidAmount), 0);
+
+ // Check the PaymentMethod from paymentMethodId returned in the Payment object
+ final UUID paymentMethodId = secondPayment.getPaymentMethodId();
+ final PaymentMethod paymentMethodJson = killBillClient.getPaymentMethod(paymentMethodId);
+ assertEquals(paymentMethodJson.getPaymentMethodId(), paymentMethodId);
+ assertEquals(paymentMethodJson.getAccountId(), accountJson.getAccountId());
+ assertEquals(paymentMethodJson.getPluginName(), ExternalPaymentProviderPlugin.PLUGIN_NAME);
+ assertNull(paymentMethodJson.getPluginInfo());
+ }
+
+ @Test(groups = "slow", description = "Can fully adjust an invoice item")
+ public void testFullInvoiceItemAdjustment() throws Exception {
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Get the invoices
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true);
+ // 2 invoices but look for the non zero dollar one
+ assertEquals(invoices.size(), 2);
+ final Invoice invoice = invoices.get(1);
+ // Verify the invoice we picked is non zero
+ assertEquals(invoice.getAmount().compareTo(BigDecimal.ZERO), 1);
+ final InvoiceItem invoiceItem = invoice.getItems().get(0);
+ // Verify the item we picked is non zero
+ assertEquals(invoiceItem.getAmount().compareTo(BigDecimal.ZERO), 1);
+
+ // Adjust the full amount
+ final InvoiceItem adjustmentInvoiceItem = new InvoiceItem();
+ adjustmentInvoiceItem.setAccountId(accountJson.getAccountId());
+ adjustmentInvoiceItem.setInvoiceId(invoice.getInvoiceId());
+ adjustmentInvoiceItem.setInvoiceItemId(invoiceItem.getInvoiceItemId());
+ killBillClient.adjustInvoiceItem(invoiceItem, createdBy, reason, comment);
+
+ // Verify the new invoice balance is zero
+ final Invoice adjustedInvoice = killBillClient.getInvoice(invoice.getInvoiceId(), true, AuditLevel.FULL);
+ assertEquals(adjustedInvoice.getAmount().compareTo(BigDecimal.ZERO), 0);
+
+ // Verify invoice audit logs
+ Assert.assertEquals(adjustedInvoice.getAuditLogs().size(), 1);
+ final AuditLog invoiceAuditLogJson = adjustedInvoice.getAuditLogs().get(0);
+ Assert.assertEquals(invoiceAuditLogJson.getChangeType(), "INSERT");
+ Assert.assertEquals(invoiceAuditLogJson.getChangedBy(), "SubscriptionBaseTransition");
+ Assert.assertNotNull(invoiceAuditLogJson.getChangeDate());
+ Assert.assertNotNull(invoiceAuditLogJson.getUserToken());
+ Assert.assertNull(invoiceAuditLogJson.getReasonCode());
+ Assert.assertNull(invoiceAuditLogJson.getComments());
+
+ Assert.assertEquals(adjustedInvoice.getItems().size(), 2);
+
+ // Verify invoice items audit logs
+
+ // The first item is the original item
+ Assert.assertEquals(adjustedInvoice.getItems().get(0).getAuditLogs().size(), 1);
+ final AuditLog itemAuditLogJson = adjustedInvoice.getItems().get(0).getAuditLogs().get(0);
+ Assert.assertEquals(itemAuditLogJson.getChangeType(), "INSERT");
+ Assert.assertEquals(itemAuditLogJson.getChangedBy(), "SubscriptionBaseTransition");
+ Assert.assertNotNull(itemAuditLogJson.getChangeDate());
+ Assert.assertNotNull(itemAuditLogJson.getUserToken());
+ Assert.assertNull(itemAuditLogJson.getReasonCode());
+ Assert.assertNull(itemAuditLogJson.getComments());
+
+ // The second one is the adjustment
+ Assert.assertEquals(adjustedInvoice.getItems().get(1).getAuditLogs().size(), 1);
+ final AuditLog adjustedItemAuditLogJson = adjustedInvoice.getItems().get(1).getAuditLogs().get(0);
+ Assert.assertEquals(adjustedItemAuditLogJson.getChangeType(), "INSERT");
+ Assert.assertEquals(adjustedItemAuditLogJson.getChangedBy(), createdBy);
+ Assert.assertEquals(adjustedItemAuditLogJson.getReasonCode(), reason);
+ Assert.assertEquals(adjustedItemAuditLogJson.getComments(), comment);
+ Assert.assertNotNull(adjustedItemAuditLogJson.getChangeDate());
+ Assert.assertNotNull(adjustedItemAuditLogJson.getUserToken());
+ }
+
+ @Test(groups = "slow", description = "Can partially adjust an invoice item")
+ public void testPartialInvoiceItemAdjustment() throws Exception {
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Get the invoices
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true);
+ // 2 invoices but look for the non zero dollar one
+ assertEquals(invoices.size(), 2);
+ final Invoice invoice = invoices.get(1);
+ // Verify the invoice we picked is non zero
+ assertEquals(invoice.getAmount().compareTo(BigDecimal.ZERO), 1);
+ final InvoiceItem invoiceItem = invoice.getItems().get(0);
+ // Verify the item we picked is non zero
+ assertEquals(invoiceItem.getAmount().compareTo(BigDecimal.ZERO), 1);
+
+ // Adjust partially the item
+ final BigDecimal adjustedAmount = invoiceItem.getAmount().divide(BigDecimal.TEN);
+ final InvoiceItem adjustmentInvoiceItem = new InvoiceItem();
+ adjustmentInvoiceItem.setAccountId(accountJson.getAccountId());
+ adjustmentInvoiceItem.setInvoiceId(invoice.getInvoiceId());
+ adjustmentInvoiceItem.setInvoiceItemId(invoiceItem.getInvoiceItemId());
+ adjustmentInvoiceItem.setAmount(adjustedAmount);
+ adjustmentInvoiceItem.setCurrency(invoice.getCurrency());
+ killBillClient.adjustInvoiceItem(adjustmentInvoiceItem, createdBy, reason, comment);
+
+ // Verify the new invoice balance
+ final Invoice adjustedInvoice = killBillClient.getInvoice(invoice.getInvoiceId());
+ final BigDecimal adjustedInvoiceBalance = invoice.getBalance().add(adjustedAmount.negate()).setScale(2, BigDecimal.ROUND_HALF_UP);
+ assertEquals(adjustedInvoice.getBalance().compareTo(adjustedInvoiceBalance), 0, String.format("Adjusted invoice balance is %s, should be %s", adjustedInvoice.getBalance(), adjustedInvoiceBalance));
+ }
+
+ @Test(groups = "slow", description = "Can create an external charge")
+ public void testExternalChargeOnNewInvoice() throws Exception {
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Get the invoices
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId()).size(), 2);
+
+ // Post an external charge
+ final BigDecimal chargeAmount = BigDecimal.TEN;
+ final InvoiceItem externalCharge = new InvoiceItem();
+ externalCharge.setAccountId(accountJson.getAccountId());
+ externalCharge.setAmount(chargeAmount);
+ final Invoice invoiceWithItems = killBillClient.createExternalCharge(externalCharge, clock.getUTCNow(), false, createdBy, reason, comment);
+ assertEquals(invoiceWithItems.getBalance().compareTo(chargeAmount), 0);
+ assertEquals(invoiceWithItems.getItems().size(), 1);
+ assertNull(invoiceWithItems.getItems().get(0).getBundleId());
+
+ // Verify the total number of invoices
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId()).size(), 3);
+ }
+
+ @Test(groups = "slow", description = "Can create an external charge and trigger a payment")
+ public void testExternalChargeOnNewInvoiceWithAutomaticPayment() throws Exception {
+ final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Get the invoices
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId()).size(), 2);
+
+ // Post an external charge
+ final BigDecimal chargeAmount = BigDecimal.TEN;
+ final InvoiceItem externalCharge = new InvoiceItem();
+ externalCharge.setAccountId(accountJson.getAccountId());
+ externalCharge.setAmount(chargeAmount);
+ final Invoice invoiceWithItems = killBillClient.createExternalCharge(externalCharge, clock.getUTCNow(), true, createdBy, reason, comment);
+ assertEquals(invoiceWithItems.getBalance().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(invoiceWithItems.getItems().size(), 1);
+ assertNull(invoiceWithItems.getItems().get(0).getBundleId());
+
+ // Verify the total number of invoices
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId()).size(), 3);
+ }
+
+ @Test(groups = "slow", description = "Can create an external charge for a bundle")
+ public void testExternalChargeForBundleOnNewInvoice() throws Exception {
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Get the invoices
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId()).size(), 2);
+
+ // Post an external charge
+ final BigDecimal chargeAmount = BigDecimal.TEN;
+ final UUID bundleId = UUID.randomUUID();
+ final InvoiceItem externalCharge = new InvoiceItem();
+ externalCharge.setAccountId(accountJson.getAccountId());
+ externalCharge.setAmount(chargeAmount);
+ externalCharge.setBundleId(bundleId);
+ final Invoice invoiceWithItems = killBillClient.createExternalCharge(externalCharge, clock.getUTCNow(), false, createdBy, reason, comment);
+ assertEquals(invoiceWithItems.getBalance().compareTo(chargeAmount), 0);
+ assertEquals(invoiceWithItems.getItems().size(), 1);
+ assertEquals(invoiceWithItems.getItems().get(0).getBundleId(), bundleId);
+
+ // Verify the total number of invoices
+ assertEquals(killBillClient.getInvoicesForAccount(accountJson.getAccountId()).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);
+ // 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.setInvoiceId(invoiceId);
+ final Invoice invoiceWithItems = killBillClient.createExternalCharge(externalCharge, clock.getUTCNow(), false, createdBy, reason, comment);
+ assertEquals(invoiceWithItems.getItems().size(), originalNumberOfItemsForInvoice + 1);
+ assertNull(invoiceWithItems.getItems().get(originalNumberOfItemsForInvoice).getBundleId());
+
+ // Verify the new invoice balance
+ final Invoice adjustedInvoice = killBillClient.getInvoice(invoiceId);
+ 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);
+ // 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.setInvoiceId(invoiceId);
+ final Invoice invoiceWithItems = killBillClient.createExternalCharge(externalCharge, clock.getUTCNow(), true, createdBy, reason, comment);
+ assertEquals(invoiceWithItems.getItems().size(), originalNumberOfItemsForInvoice + 1);
+ assertNull(invoiceWithItems.getItems().get(originalNumberOfItemsForInvoice).getBundleId());
+
+ // Verify the new invoice balance
+ final Invoice adjustedInvoice = killBillClient.getInvoice(invoiceId);
+ 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);
+ // 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.setInvoiceId(invoiceId);
+ externalCharge.setBundleId(bundleId);
+ final Invoice invoiceWithItems = killBillClient.createExternalCharge(externalCharge, clock.getUTCNow(), false, createdBy, reason, comment);
+ assertEquals(invoiceWithItems.getItems().size(), originalNumberOfItemsForInvoice + 1);
+ assertEquals(invoiceWithItems.getItems().get(originalNumberOfItemsForInvoice).getBundleId(), bundleId);
+
+ // Verify the new invoice balance
+ final Invoice adjustedInvoice = killBillClient.getInvoice(invoiceId);
+ 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();
+
+ for (int i = 0; i < 3; i++) {
+ clock.addMonths(1);
+ crappyWaitForLackOfProperSynchonization();
+ }
+
+ final Invoices allInvoices = killBillClient.getInvoices();
+ Assert.assertEquals(allInvoices.size(), 5);
+
+ for (final Invoice invoice : allInvoices) {
+ Assert.assertEquals(killBillClient.searchInvoices(invoice.getInvoiceId().toString()).size(), 1);
+ Assert.assertEquals(killBillClient.searchInvoices(invoice.getAccountId().toString()).size(), 5);
+ Assert.assertEquals(killBillClient.searchInvoices(invoice.getInvoiceNumber().toString()).size(), 1);
+ Assert.assertEquals(killBillClient.searchInvoices(invoice.getCurrency().toString()).size(), 5);
+ }
+
+ Invoices page = killBillClient.getInvoices(0L, 1L);
+ for (int i = 0; i < 5; i++) {
+ Assert.assertNotNull(page);
+ Assert.assertEquals(page.size(), 1);
+ Assert.assertEquals(page.get(0), allInvoices.get(i));
+ page = page.getNext();
+ }
+ Assert.assertNull(page);
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestInvoiceNotification.java b/server/src/test/java/org/killbill/billing/jaxrs/TestInvoiceNotification.java
new file mode 100644
index 0000000..1807f44
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestInvoiceNotification.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Invoice;
+import org.killbill.billing.client.model.Subscription;
+
+public class TestInvoiceNotification extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can trigger an invoice notification")
+ public void testTriggerNotification() throws Exception {
+ final Account accountJson = createScenarioWithOneInvoice();
+
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ Assert.assertEquals(invoices.size(), 1);
+
+ final Invoice invoice = invoices.get(0);
+ killBillClient.triggerInvoiceNotification(invoice.getInvoiceId(), createdBy, reason, comment);
+ }
+
+ private Account createScenarioWithOneInvoice() throws Exception {
+ final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+ Assert.assertNotNull(accountJson);
+
+ final Subscription subscriptionJson = createEntitlement(accountJson.getAccountId(), "76213", "Shotgun",
+ ProductCategory.BASE, BillingPeriod.MONTHLY, true);
+ Assert.assertNotNull(subscriptionJson);
+
+ return accountJson;
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java b/server/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
new file mode 100644
index 0000000..a1845ec
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.EventListener;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
+
+import org.apache.shiro.web.servlet.ShiroFilter;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.joda.time.LocalDate;
+import org.killbill.billing.DBTestingHelper;
+import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
+import org.killbill.billing.KillbillConfigSource;
+import org.killbill.billing.account.glue.DefaultAccountModule;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.beatrix.glue.BeatrixModule;
+import org.killbill.billing.catalog.glue.CatalogModule;
+import org.killbill.billing.client.KillBillClient;
+import org.killbill.billing.client.KillBillHttpClient;
+import org.killbill.billing.client.model.Tenant;
+import org.killbill.billing.currency.glue.CurrencyModule;
+import org.killbill.billing.entitlement.glue.DefaultEntitlementModule;
+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.junction.glue.DefaultJunctionModule;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.osgi.glue.DefaultOSGIModule;
+import org.killbill.billing.overdue.glue.DefaultOverdueModule;
+import org.killbill.billing.payment.glue.PaymentModule;
+import org.killbill.billing.payment.provider.MockPaymentProviderPluginModule;
+import org.killbill.billing.server.config.DaoConfig;
+import org.killbill.billing.server.listeners.KillbillGuiceListener;
+import org.killbill.billing.server.modules.KillBillShiroWebModule;
+import org.killbill.billing.server.modules.KillbillServerModule;
+import org.killbill.billing.subscription.glue.DefaultSubscriptionModule;
+import org.killbill.billing.tenant.glue.TenantModule;
+import org.killbill.billing.usage.glue.UsageModule;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.config.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.BusModule;
+import org.killbill.billing.util.glue.CacheModule;
+import org.killbill.billing.util.glue.CallContextModule;
+import org.killbill.billing.util.glue.CustomFieldModule;
+import org.killbill.billing.util.glue.ExportModule;
+import org.killbill.billing.util.glue.GlobalLockerModule;
+import org.killbill.billing.util.glue.KillBillShiroAopModule;
+import org.killbill.billing.util.glue.NonEntityDaoModule;
+import org.killbill.billing.util.glue.NotificationQueueModule;
+import org.killbill.billing.util.glue.RecordIdModule;
+import org.killbill.billing.util.glue.SecurityModule;
+import org.killbill.billing.util.glue.TagStoreModule;
+import org.killbill.bus.api.PersistentBus;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.AfterSuite;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeSuite;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Module;
+
+import static org.testng.Assert.assertNotNull;
+
+public class TestJaxrsBase extends KillbillClient {
+
+ protected static final String PLUGIN_NAME = "noop";
+
+ @Inject
+ protected OSGIServiceRegistration<Servlet> servletRouter;
+
+ @Inject
+ protected CacheControllerDispatcher cacheControllerDispatcher;
+
+ @Inject
+ protected @javax.inject.Named(BeatrixModule.EXTERNAL_BUS)
+ PersistentBus externalBus;
+
+ @Inject
+ protected PersistentBus internalBus;
+
+ @Inject
+ protected TestApiListener busHandler;
+
+ protected static TestKillbillGuiceListener listener;
+
+ protected HttpServerConfig config;
+ private HttpServer server;
+
+ public static void loadSystemPropertiesFromClasspath(final String resource) {
+ final URL url = TestJaxrsBase.class.getResource(resource);
+ assertNotNull(url);
+ try {
+ System.getProperties().load(url.openStream());
+ } catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static class TestKillbillGuiceListener extends KillbillGuiceListener {
+
+ private final DaoConfig daoConfig;
+
+ public TestKillbillGuiceListener(final DaoConfig daoConfig) {
+ super();
+ this.daoConfig = daoConfig;
+ }
+
+ @Override
+ protected Module getModule(final ServletContext servletContext) {
+ return new TestKillbillServerModule(daoConfig, servletContext);
+ }
+
+ }
+
+ public static class InvoiceModuleWithMockSender extends DefaultInvoiceModule {
+
+ public InvoiceModuleWithMockSender(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void installInvoiceNotifier() {
+ bind(InvoiceNotifier.class).to(NullInvoiceNotifier.class).asEagerSingleton();
+ }
+ }
+
+ public static class TestKillbillServerModule extends KillbillServerModule {
+
+ public TestKillbillServerModule(final DaoConfig daoConfig, final ServletContext servletContext) {
+ super(servletContext, daoConfig, false);
+ }
+
+ @Override
+ protected void installClock() {
+ // Already done By Top test class
+ }
+
+ @Override
+ protected void configureDao() {
+ // Already done By Top test class
+ }
+
+ private static final class PaymentMockModule extends PaymentModule {
+
+ public PaymentMockModule(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void installPaymentProviderPlugins(final PaymentConfig config) {
+ install(new MockPaymentProviderPluginModule(PLUGIN_NAME, getClock()));
+ }
+ }
+
+ @Override
+ protected void installKillbillModules() {
+ final KillbillConfigSource configSource = new KillbillConfigSource(System.getProperties());
+
+ /*
+ * For a lack of getting module override working, copy all install modules from parent class...
+ *
+ super.installKillbillModules();
+ Modules.override(new org.killbill.billing.payment.setup.PaymentModule()).with(new PaymentMockModule());
+ */
+
+ install(new GuicyKillbillTestWithEmbeddedDBModule());
+
+ install(new EmailModule(configSource));
+ install(new CacheModule(configSource));
+ install(new NonEntityDaoModule());
+ install(new GlobalLockerModule(DBTestingHelper.get().getDBEngine()));
+ install(new CustomFieldModule());
+ install(new TagStoreModule());
+ install(new AuditModule());
+ install(new CatalogModule(configSource));
+ install(new BusModule(configSource));
+ install(new NotificationQueueModule(configSource));
+ install(new CallContextModule());
+ install(new DefaultAccountModule(configSource));
+ install(new InvoiceModuleWithMockSender(configSource));
+ install(new TemplateModule());
+ install(new DefaultSubscriptionModule(configSource));
+ install(new DefaultEntitlementModule(configSource));
+ install(new PaymentMockModule(configSource));
+ install(new BeatrixModule(configSource));
+ install(new DefaultJunctionModule(configSource));
+ install(new DefaultOverdueModule(configSource));
+ install(new TenantModule(configSource));
+ install(new CurrencyModule(configSource));
+ install(new ExportModule());
+ install(new DefaultOSGIModule(configSource));
+ install(new UsageModule(configSource));
+ install(new RecordIdModule());
+ installClock();
+ install(new KillBillShiroWebModule(servletContext, configSource));
+ install(new KillBillShiroAopModule());
+ install(new SecurityModule());
+ }
+ }
+
+ protected void setupClient(final String username, final String password, final String apiKey, final String apiSecret) {
+ killBillHttpClient = new KillBillHttpClient(String.format("http://%s:%d", config.getServerHost(), config.getServerPort()),
+ username,
+ password,
+ apiKey,
+ apiSecret);
+ killBillClient = new KillBillClient(killBillHttpClient);
+ }
+
+ protected void loginTenant(final String apiKey, final String apiSecret) {
+ setupClient(USERNAME, PASSWORD, apiKey, apiSecret);
+ }
+
+ protected void logoutTenant() {
+ setupClient(USERNAME, PASSWORD, null, null);
+ }
+
+ protected void login() {
+ login(USERNAME, PASSWORD);
+ }
+
+ protected void login(final String username, final String password) {
+ setupClient(username, password, DEFAULT_API_KEY, DEFAULT_API_SECRET);
+ }
+
+ protected void logout() {
+ setupClient(null, null, null, null);
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ externalBus.start();
+ internalBus.start();
+ cacheControllerDispatcher.clearAll();
+ busHandler.reset();
+ clock.resetDeltaFromReality();
+ clock.setDay(new LocalDate(2012, 8, 25));
+
+ loginTenant(DEFAULT_API_KEY, DEFAULT_API_SECRET);
+
+ // Recreate the tenant (tables have been cleaned-up)
+ final Tenant tenant = new Tenant();
+ tenant.setApiKey(DEFAULT_API_KEY);
+ tenant.setApiSecret(DEFAULT_API_SECRET);
+ killBillClient.createTenant(tenant, createdBy, reason, comment);
+ }
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ killBillClient.close();
+ externalBus.stop();
+ internalBus.stop();
+ }
+
+ @BeforeClass(groups = "slow")
+ public void beforeClass() throws Exception {
+ loadConfig();
+
+ listener.getInstantiatedInjector().injectMembers(this);
+ }
+
+ protected void loadConfig() {
+ if (config == null) {
+ config = new ConfigurationObjectFactory(System.getProperties()).build(HttpServerConfig.class);
+ }
+
+ // For shiro (outside of Guice control)
+ System.setProperty("org.killbill.dao.url", DBTestingHelper.get().getJdbcConnectionString());
+ System.setProperty("org.killbill.dao.user", DBTestingHelper.get().getUsername());
+ System.setProperty("org.killbill.dao.password", DBTestingHelper.get().getPassword());
+ }
+
+ @BeforeSuite(groups = "slow")
+ public void beforeSuite() throws Exception {
+ super.beforeSuite();
+ loadSystemPropertiesFromClasspath("/killbill.properties");
+ loadConfig();
+
+ listener = new TestKillbillGuiceListener(new ConfigurationObjectFactory(System.getProperties()).build(DaoConfig.class));
+
+ server = new HttpServer();
+ server.configure(config, getListeners(), getFilters());
+ server.start();
+ }
+
+ protected Iterable<EventListener> getListeners() {
+ return new Iterable<EventListener>() {
+ @Override
+ public Iterator<EventListener> iterator() {
+ // Note! This needs to be in sync with web.xml
+ return ImmutableList.<EventListener>of(listener).iterator();
+ }
+ };
+ }
+
+ protected Map<FilterHolder, String> getFilters() {
+ // Note! This needs to be in sync with web.xml
+ return ImmutableMap.<FilterHolder, String>of(new FilterHolder(new ShiroFilter()), "/*");
+ }
+
+ @AfterSuite(groups = "slow")
+ public void afterSuite() {
+ try {
+ server.stop();
+ } catch (final Exception ignored) {
+ }
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestJetty.java b/server/src/test/java/org/killbill/billing/jaxrs/TestJetty.java
new file mode 100644
index 0000000..0ad1887
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestJetty.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.jaxrs;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+import com.google.common.io.CharStreams;
+
+public class TestJetty {
+
+ public TestJetty() {
+
+ }
+
+ public static void main(final String [] args) throws Exception {
+
+ final Server server = new Server(8080);
+
+ final ServletContextHandler context = new ServletContextHandler();
+ context.setContextPath("/");
+ server.setHandler(context);
+
+ context.addServlet(new ServletHolder(new CallmebackServlet()),"/callmeback");
+
+ server.start();
+ server.join();
+ }
+
+
+ public static class CallmebackServlet extends HttpServlet
+ {
+ public CallmebackServlet() {
+ }
+
+ @Override
+ protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
+ {
+ final String body = CharStreams.toString( new InputStreamReader(request.getInputStream(), "UTF-8" ));
+ System.out.print("Got " + body);
+
+
+ response.setContentType("application/json");
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getWriter().println("{\"key\"=12}");
+ }
+ }
+
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java b/server/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
new file mode 100644
index 0000000..a473a9d
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Invoice;
+import org.killbill.billing.client.model.Payment;
+
+import static org.testng.Assert.assertEquals;
+
+public class TestOverdue extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can retrieve the account overdue status")
+ public void testOverdueStatus() throws Exception {
+ // Create an account without a payment method
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ // Get the invoices
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+ // 2 invoices but look for the non zero dollar one
+ assertEquals(invoices.size(), 2);
+
+ // We're still clear - see the configuration
+ Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getIsClearState());
+
+ clock.addDays(30);
+ crappyWaitForLackOfProperSynchonization();
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD1");
+
+ clock.addDays(10);
+ crappyWaitForLackOfProperSynchonization();
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD2");
+
+ clock.addDays(10);
+ crappyWaitForLackOfProperSynchonization();
+ Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD3");
+
+ // Post external payments
+ for (final Invoice invoice : killBillClient.getInvoicesForAccount(accountJson.getAccountId())) {
+ if (invoice.getBalance().compareTo(BigDecimal.ZERO) > 0) {
+ final Payment payment = new Payment();
+ payment.setAccountId(accountJson.getAccountId());
+ payment.setInvoiceId(invoice.getInvoiceId());
+ payment.setAmount(invoice.getBalance());
+ killBillClient.createPayment(payment, true, createdBy, reason, comment);
+ }
+ }
+
+ // Wait a bit for overdue to pick up the payment events...
+ crappyWaitForLackOfProperSynchonization();
+
+ // Verify we're in clear state
+ Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getIsClearState());
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestPayment.java b/server/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
new file mode 100644
index 0000000..973d7f2
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Invoice;
+import org.killbill.billing.client.model.InvoiceItem;
+import org.killbill.billing.client.model.Payment;
+import org.killbill.billing.client.model.PaymentMethod;
+import org.killbill.billing.client.model.Payments;
+import org.killbill.billing.client.model.Refund;
+import org.killbill.billing.client.model.Refunds;
+import org.killbill.billing.payment.api.RefundStatus;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestPayment extends TestJaxrsBase {
+
+ @Test(groups = "slow")
+ public void testRetrievePayment() throws Exception {
+ final Payment paymentJson = setupScenarioWithPayment();
+
+ final Payment retrievedPaymentJson = killBillClient.getPayment(paymentJson.getPaymentId(), false);
+ Assert.assertEquals(retrievedPaymentJson, paymentJson);
+ }
+
+ @Test(groups = "slow", description = "Can create a full refund with no adjustment")
+ public void testFullRefundWithNoAdjustment() throws Exception {
+ final Payment paymentJson = setupScenarioWithPayment();
+
+ // Issue a refund for the full amount
+ final BigDecimal refundAmount = paymentJson.getAmount();
+ final BigDecimal expectedInvoiceBalance = refundAmount;
+
+ // Post and verify the refund
+ final Refund refund = new Refund();
+ refund.setPaymentId(paymentJson.getPaymentId());
+ refund.setAmount(refundAmount);
+ final Refund refundJsonCheck = killBillClient.createRefund(refund, createdBy, reason, comment);
+ verifyRefund(paymentJson, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJson, expectedInvoiceBalance);
+ }
+
+ @Test(groups = "slow", description = "Can create a partial refund with no adjustment")
+ public void testPartialRefundWithNoAdjustment() throws Exception {
+ final Payment paymentJson = setupScenarioWithPayment();
+
+ // Issue a refund for a fraction of the amount
+ final BigDecimal refundAmount = getFractionOfAmount(paymentJson.getAmount());
+ final BigDecimal expectedInvoiceBalance = refundAmount;
+
+ // Post and verify the refund
+ final Refund refund = new Refund();
+ refund.setPaymentId(paymentJson.getPaymentId());
+ refund.setAmount(refundAmount);
+ final Refund refundJsonCheck = killBillClient.createRefund(refund, createdBy, reason, comment);
+ verifyRefund(paymentJson, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJson, expectedInvoiceBalance);
+ }
+
+ @Test(groups = "slow", description = "Can create a full refund with invoice adjustment")
+ public void testFullRefundWithInvoiceAdjustment() throws Exception {
+ final Payment paymentJson = setupScenarioWithPayment();
+
+ // Issue a refund for the full amount
+ final BigDecimal refundAmount = paymentJson.getAmount();
+ final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+ // Post and verify the refund
+ final Refund refund = new Refund();
+ refund.setPaymentId(paymentJson.getPaymentId());
+ refund.setAmount(refundAmount);
+ refund.setAdjusted(true);
+ final Refund refundJsonCheck = killBillClient.createRefund(refund, createdBy, reason, comment);
+ verifyRefund(paymentJson, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJson, expectedInvoiceBalance);
+ }
+
+ @Test(groups = "slow", description = "Can create a partial refund with invoice adjustment")
+ public void testPartialRefundWithInvoiceAdjustment() throws Exception {
+ final Payment paymentJson = setupScenarioWithPayment();
+
+ // Issue a refund for a fraction of the amount
+ final BigDecimal refundAmount = getFractionOfAmount(paymentJson.getAmount());
+ final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+ // Post and verify the refund
+ final Refund refund = new Refund();
+ refund.setPaymentId(paymentJson.getPaymentId());
+ refund.setAmount(refundAmount);
+ refund.setAdjusted(true);
+ final Refund refundJsonCheck = killBillClient.createRefund(refund, createdBy, reason, comment);
+ verifyRefund(paymentJson, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJson, expectedInvoiceBalance);
+ }
+
+ @Test(groups = "slow", description = "Can create a full refund with invoice item adjustment")
+ public void testRefundWithFullInvoiceItemAdjustment() throws Exception {
+ final Payment paymentJson = setupScenarioWithPayment();
+
+ // Get the individual items for the invoice
+ final Invoice invoice = killBillClient.getInvoice(paymentJson.getInvoiceId(), true);
+ final InvoiceItem itemToAdjust = invoice.getItems().get(0);
+
+ // Issue a refund for the full amount
+ final BigDecimal refundAmount = itemToAdjust.getAmount();
+ final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+ // Post and verify the refund
+ final Refund refund = new Refund();
+ refund.setPaymentId(paymentJson.getPaymentId());
+ refund.setAmount(refundAmount);
+ refund.setAdjusted(true);
+ final InvoiceItem adjustment = new InvoiceItem();
+ adjustment.setInvoiceItemId(itemToAdjust.getInvoiceItemId());
+ /* null amount means full adjustment for that item */
+ refund.setAdjustments(ImmutableList.<InvoiceItem>of(adjustment));
+ final Refund refundJsonCheck = killBillClient.createRefund(refund, createdBy, reason, comment);
+ verifyRefund(paymentJson, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJson, expectedInvoiceBalance);
+ }
+
+ @Test(groups = "slow", description = "Can create a partial refund with invoice item adjustment")
+ public void testPartialRefundWithInvoiceItemAdjustment() throws Exception {
+ final Payment paymentJson = setupScenarioWithPayment();
+
+ // Get the individual items for the invoice
+ final Invoice invoice = killBillClient.getInvoice(paymentJson.getInvoiceId(), true);
+ final InvoiceItem itemToAdjust = invoice.getItems().get(0);
+
+ // Issue a refund for a fraction of the amount
+ final BigDecimal refundAmount = getFractionOfAmount(itemToAdjust.getAmount());
+ final BigDecimal expectedInvoiceBalance = BigDecimal.ZERO;
+
+ // Post and verify the refund
+ final Refund refund = new Refund();
+ refund.setPaymentId(paymentJson.getPaymentId());
+ refund.setAdjusted(true);
+ final InvoiceItem adjustment = new InvoiceItem();
+ adjustment.setInvoiceItemId(itemToAdjust.getInvoiceItemId());
+ adjustment.setAmount(refundAmount);
+ refund.setAdjustments(ImmutableList.<InvoiceItem>of(adjustment));
+ final Refund refundJsonCheck = killBillClient.createRefund(refund, createdBy, reason, comment);
+ verifyRefund(paymentJson, refundJsonCheck, refundAmount);
+
+ // Verify the invoice balance
+ verifyInvoice(paymentJson, expectedInvoiceBalance);
+ }
+
+ @Test(groups = "slow", description = "Can paginate through all payments and refunds")
+ public void testPaymentsAndRefundsPagination() throws Exception {
+ Payment lastPayment = setupScenarioWithPayment();
+
+ for (int i = 0; i < 5; i++) {
+ final Refund refund = new Refund();
+ refund.setPaymentId(lastPayment.getPaymentId());
+ refund.setAmount(lastPayment.getAmount());
+ killBillClient.createRefund(refund, createdBy, reason, comment);
+
+ final Payment payment = new Payment();
+ payment.setAccountId(lastPayment.getAccountId());
+ payment.setInvoiceId(lastPayment.getInvoiceId());
+ payment.setAmount(lastPayment.getAmount());
+ final List<Payment> payments = killBillClient.createPayment(payment, false, createdBy, reason, comment);
+
+ lastPayment = payments.get(payments.size() - 1);
+ }
+
+ final Payments allPayments = killBillClient.getPayments();
+ Assert.assertEquals(allPayments.size(), 6);
+
+ final Refunds allRefunds = killBillClient.getRefunds();
+ Assert.assertEquals(allRefunds.size(), 5);
+
+ Payments paymentsPage = killBillClient.getPayments(0L, 1L);
+ for (int i = 0; i < 6; i++) {
+ Assert.assertNotNull(paymentsPage);
+ Assert.assertEquals(paymentsPage.size(), 1);
+ Assert.assertEquals(paymentsPage.get(0), allPayments.get(i));
+ paymentsPage = paymentsPage.getNext();
+ }
+ Assert.assertNull(paymentsPage);
+
+ Refunds refundsPage = killBillClient.getRefunds(0L, 1L);
+ for (int i = 0; i < 5; i++) {
+ Assert.assertNotNull(refundsPage);
+ Assert.assertEquals(refundsPage.size(), 1);
+ Assert.assertEquals(refundsPage.get(0), allRefunds.get(i));
+ refundsPage = refundsPage.getNext();
+ }
+ Assert.assertNull(refundsPage);
+ }
+
+ private BigDecimal getFractionOfAmount(final BigDecimal amount) {
+ return amount.divide(BigDecimal.TEN).setScale(2, BigDecimal.ROUND_HALF_UP);
+ }
+
+ private Payment setupScenarioWithPayment() throws Exception {
+ final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
+
+ final List<Payment> firstPaymentForAccount = killBillClient.getPaymentsForAccount(accountJson.getAccountId());
+ Assert.assertEquals(firstPaymentForAccount.size(), 1);
+
+ final Payment paymentJson = firstPaymentForAccount.get(0);
+
+ // Check the PaymentMethod from paymentMethodId returned in the Payment object
+ final UUID paymentMethodId = paymentJson.getPaymentMethodId();
+ final PaymentMethod paymentMethodJson = killBillClient.getPaymentMethod(paymentMethodId, true);
+ Assert.assertEquals(paymentMethodJson.getPaymentMethodId(), paymentMethodId);
+ Assert.assertEquals(paymentMethodJson.getAccountId(), accountJson.getAccountId());
+
+ // Verify the refunds
+ final List<Refund> objRefundFromJson = killBillClient.getRefundsForPayment(paymentJson.getPaymentId());
+ Assert.assertEquals(objRefundFromJson.size(), 0);
+ return paymentJson;
+ }
+
+ private void verifyRefund(final Payment paymentJson, final Refund refundJsonCheck, final BigDecimal refundAmount) throws KillBillClientException {
+ Assert.assertEquals(refundJsonCheck.getPaymentId(), paymentJson.getPaymentId());
+ Assert.assertEquals(refundJsonCheck.getAmount().setScale(2, RoundingMode.HALF_UP), refundAmount.setScale(2, RoundingMode.HALF_UP));
+ Assert.assertEquals(refundJsonCheck.getCurrency(), DEFAULT_CURRENCY);
+ Assert.assertEquals(refundJsonCheck.getStatus(), RefundStatus.COMPLETED.toString());
+ Assert.assertEquals(refundJsonCheck.getEffectiveDate().getYear(), clock.getUTCNow().getYear());
+ Assert.assertEquals(refundJsonCheck.getEffectiveDate().getMonthOfYear(), clock.getUTCNow().getMonthOfYear());
+ Assert.assertEquals(refundJsonCheck.getEffectiveDate().getDayOfMonth(), clock.getUTCNow().getDayOfMonth());
+ Assert.assertEquals(refundJsonCheck.getRequestedDate().getYear(), clock.getUTCNow().getYear());
+ Assert.assertEquals(refundJsonCheck.getRequestedDate().getMonthOfYear(), clock.getUTCNow().getMonthOfYear());
+ Assert.assertEquals(refundJsonCheck.getRequestedDate().getDayOfMonth(), clock.getUTCNow().getDayOfMonth());
+
+ // Verify the refunds
+ final List<Refund> retrievedRefunds = killBillClient.getRefundsForPayment(paymentJson.getPaymentId());
+ Assert.assertEquals(retrievedRefunds.size(), 1);
+
+ // Verify the refund via the payment API
+ final Payment retrievedPaymentJson = killBillClient.getPayment(paymentJson.getPaymentId(), true);
+ Assert.assertEquals(retrievedPaymentJson.getPaymentId(), paymentJson.getPaymentId());
+ Assert.assertEquals(retrievedPaymentJson.getPaidAmount().setScale(2, RoundingMode.HALF_UP), paymentJson.getPaidAmount().add(refundAmount.negate()).setScale(2, RoundingMode.HALF_UP));
+ Assert.assertEquals(retrievedPaymentJson.getAmount().setScale(2, RoundingMode.HALF_UP), paymentJson.getAmount().setScale(2, RoundingMode.HALF_UP));
+ Assert.assertEquals(retrievedPaymentJson.getAccountId(), paymentJson.getAccountId());
+ Assert.assertEquals(retrievedPaymentJson.getInvoiceId(), paymentJson.getInvoiceId());
+ Assert.assertEquals(retrievedPaymentJson.getRequestedDate(), paymentJson.getRequestedDate());
+ Assert.assertEquals(retrievedPaymentJson.getEffectiveDate(), paymentJson.getEffectiveDate());
+ Assert.assertEquals(retrievedPaymentJson.getRetryCount(), paymentJson.getRetryCount());
+ Assert.assertEquals(retrievedPaymentJson.getCurrency(), paymentJson.getCurrency());
+ Assert.assertEquals(retrievedPaymentJson.getStatus(), paymentJson.getStatus());
+ Assert.assertEquals(retrievedPaymentJson.getGatewayErrorCode(), paymentJson.getGatewayErrorCode());
+ Assert.assertEquals(retrievedPaymentJson.getGatewayErrorMsg(), paymentJson.getGatewayErrorMsg());
+ Assert.assertEquals(retrievedPaymentJson.getPaymentMethodId(), paymentJson.getPaymentMethodId());
+ Assert.assertEquals(retrievedPaymentJson.getChargebacks().size(), 0);
+ Assert.assertEquals(retrievedPaymentJson.getRefunds().size(), 1);
+ Assert.assertEquals(retrievedPaymentJson.getRefunds().get(0), refundJsonCheck);
+ }
+
+ private void verifyInvoice(final Payment paymentJson, final BigDecimal expectedInvoiceBalance) throws KillBillClientException {
+ final Invoice invoiceJson = killBillClient.getInvoice(paymentJson.getInvoiceId());
+ Assert.assertEquals(invoiceJson.getBalance().setScale(2, BigDecimal.ROUND_HALF_UP),
+ expectedInvoiceBalance.setScale(2, BigDecimal.ROUND_HALF_UP));
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestPaymentMethod.java b/server/src/test/java/org/killbill/billing/jaxrs/TestPaymentMethod.java
new file mode 100644
index 0000000..c1c8258
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestPaymentMethod.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.PaymentMethod;
+import org.killbill.billing.client.model.PaymentMethods;
+
+public class TestPaymentMethod extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Can search payment methods")
+ public void testSearchPaymentMethods() throws Exception {
+ // Search random key
+ Assert.assertEquals(killBillClient.searchPaymentMethodsByKey(UUID.randomUUID().toString()).size(), 0);
+ Assert.assertEquals(killBillClient.searchPaymentMethodsByKeyAndPlugin(UUID.randomUUID().toString(), PLUGIN_NAME).size(), 0);
+
+ // Create a payment method
+ final Account accountJson = createAccountWithDefaultPaymentMethod();
+ final PaymentMethod paymentMethodJson = killBillClient.getPaymentMethod(accountJson.getPaymentMethodId(), true);
+
+ // Search random key again
+ Assert.assertEquals(killBillClient.searchPaymentMethodsByKey(UUID.randomUUID().toString()).size(), 0);
+ Assert.assertEquals(killBillClient.searchPaymentMethodsByKeyAndPlugin(UUID.randomUUID().toString(), PLUGIN_NAME).size(), 0);
+
+ // Make sure we can search the test plugin
+ // Values are hardcoded in TestPaymentMethodPluginBase and the search logic is in MockPaymentProviderPlugin
+ doSearch("Foo", paymentMethodJson);
+ // Last 4
+ doSearch("4365", paymentMethodJson);
+ // Name
+ doSearch("Bozo", paymentMethodJson);
+ // City
+ doSearch("SF", paymentMethodJson);
+ // State
+ doSearch("CA", paymentMethodJson);
+ // Country
+ doSearch("Zimbawe", paymentMethodJson);
+ }
+
+ @Test(groups = "slow", description = "Can paginate through all payment methods")
+ public void testPaymentMethodsPagination() throws Exception {
+ for (int i = 0; i < 5; i++) {
+ createAccountWithDefaultPaymentMethod();
+ }
+
+ final PaymentMethods allPaymentMethods = killBillClient.getPaymentMethods();
+ Assert.assertEquals(allPaymentMethods.size(), 5);
+
+ PaymentMethods page = killBillClient.getPaymentMethods(0L, 1L);
+ for (int i = 0; i < 5; i++) {
+ Assert.assertNotNull(page);
+ Assert.assertEquals(page.size(), 1);
+ Assert.assertEquals(page.get(0), allPaymentMethods.get(i));
+ page = page.getNext();
+ }
+ Assert.assertNull(page);
+ }
+
+ private void doSearch(final String searchKey, final PaymentMethod paymentMethodJson) throws Exception {
+ final List<PaymentMethod> results1 = killBillClient.searchPaymentMethodsByKey(searchKey);
+ Assert.assertEquals(results1.size(), 1);
+ Assert.assertEquals(results1.get(0), paymentMethodJson);
+
+ final List<PaymentMethod> results2 = killBillClient.searchPaymentMethodsByKeyAndPlugin(searchKey, PLUGIN_NAME);
+ Assert.assertEquals(results2.size(), 1);
+ Assert.assertEquals(results2.get(0), paymentMethodJson);
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestPlugin.java b/server/src/test/java/org/killbill/billing/jaxrs/TestPlugin.java
new file mode 100644
index 0000000..7fdae9e
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestPlugin.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nullable;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.osgi.http.DefaultServletRouter;
+import com.ning.http.client.Response;
+
+public class TestPlugin extends TestJaxrsBase {
+
+ private static final String TEST_PLUGIN_NAME = "test-osgi";
+
+ private static final byte[] TEST_PLUGIN_RESPONSE_BYTES = new byte[]{0xC, 0x0, 0xF, 0xF, 0xE, 0xE};
+
+ private static final String TEST_PLUGIN_VALID_GET_PATH = "setGETMarkerToTrue";
+ private static final String TEST_PLUGIN_VALID_HEAD_PATH = "setHEADMarkerToTrue";
+ private static final String TEST_PLUGIN_VALID_POST_PATH = "setPOSTMarkerToTrue";
+ private static final String TEST_PLUGIN_VALID_PUT_PATH = "setPUTMarkerToTrue";
+ private static final String TEST_PLUGIN_VALID_DELETE_PATH = "setDELETEMarkerToTrue";
+ private static final String TEST_PLUGIN_VALID_OPTIONS_PATH = "setOPTIONSMarkerToTrue";
+
+ private final AtomicBoolean requestGETMarker = new AtomicBoolean(false);
+ private final AtomicBoolean requestHEADMarker = new AtomicBoolean(false);
+ private final AtomicBoolean requestPOSTMarker = new AtomicBoolean(false);
+ private final AtomicBoolean requestPUTMarker = new AtomicBoolean(false);
+ private final AtomicBoolean requestDELETEMarker = new AtomicBoolean(false);
+ private final AtomicBoolean requestOPTIONSMarker = new AtomicBoolean(false);
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ setupOSGIPlugin();
+ resetAllMarkers();
+ }
+
+ @Test(groups = "slow")
+ public void testPassRequestsToUnknownPlugin() throws Exception {
+ final String uri = "pluginDoesNotExist/something";
+ Response response;
+
+ // We don't test the output here as it is some Jetty specific HTML blurb
+
+ response = killBillClient.pluginGET(uri);
+ testAndResetAllMarkers(response, 404, null, false, false, false, false, false, false);
+
+ response = killBillClient.pluginHEAD(uri);
+ testAndResetAllMarkers(response, 404, null, false, false, false, false, false, false);
+
+ response = killBillClient.pluginPOST(uri, null);
+ testAndResetAllMarkers(response, 404, null, false, false, false, false, false, false);
+
+ response = killBillClient.pluginPUT(uri, null);
+ testAndResetAllMarkers(response, 404, null, false, false, false, false, false, false);
+
+ response = killBillClient.pluginDELETE(uri);
+ testAndResetAllMarkers(response, 404, null, false, false, false, false, false, false);
+
+ response = killBillClient.pluginOPTIONS(uri);
+ testAndResetAllMarkers(response, 404, null, false, false, false, false, false, false);
+ }
+
+ @Test(groups = "slow")
+ public void testPassRequestsToKnownPluginButWrongPath() throws Exception {
+ final String uri = TEST_PLUGIN_NAME + "/somethingSomething";
+ Response response;
+
+ response = killBillClient.pluginGET(uri);
+ testAndResetAllMarkers(response, 200, new byte[]{}, false, false, false, false, false, false);
+
+ response = killBillClient.pluginHEAD(uri);
+ testAndResetAllMarkers(response, 204, new byte[]{}, false, false, false, false, false, false);
+
+ response = killBillClient.pluginPOST(uri, null);
+ testAndResetAllMarkers(response, 200, new byte[]{}, false, false, false, false, false, false);
+
+ response = killBillClient.pluginPUT(uri, null);
+ testAndResetAllMarkers(response, 200, new byte[]{}, false, false, false, false, false, false);
+
+ response = killBillClient.pluginDELETE(uri);
+ testAndResetAllMarkers(response, 200, new byte[]{}, false, false, false, false, false, false);
+
+ response = killBillClient.pluginOPTIONS(uri);
+ testAndResetAllMarkers(response, 200, new byte[]{}, false, false, false, false, false, false);
+ }
+
+ @Test(groups = "slow")
+ public void testPassRequestsToKnownPluginAndKnownPath() throws Exception {
+ Response response;
+
+ response = killBillClient.pluginGET(TEST_PLUGIN_NAME + "/" + TEST_PLUGIN_VALID_GET_PATH);
+ testAndResetAllMarkers(response, 230, TEST_PLUGIN_RESPONSE_BYTES, true, false, false, false, false, false);
+
+ response = killBillClient.pluginHEAD(TEST_PLUGIN_NAME + "/" + TEST_PLUGIN_VALID_HEAD_PATH);
+ testAndResetAllMarkers(response, 204, new byte[]{}, false, true, false, false, false, false);
+
+ response = killBillClient.pluginPOST(TEST_PLUGIN_NAME + "/" + TEST_PLUGIN_VALID_POST_PATH, null);
+ testAndResetAllMarkers(response, 230, TEST_PLUGIN_RESPONSE_BYTES, false, false, true, false, false, false);
+
+ response = killBillClient.pluginPUT(TEST_PLUGIN_NAME + "/" + TEST_PLUGIN_VALID_PUT_PATH, null);
+ testAndResetAllMarkers(response, 230, TEST_PLUGIN_RESPONSE_BYTES, false, false, false, true, false, false);
+
+ response = killBillClient.pluginDELETE(TEST_PLUGIN_NAME + "/" + TEST_PLUGIN_VALID_DELETE_PATH);
+ testAndResetAllMarkers(response, 230, TEST_PLUGIN_RESPONSE_BYTES, false, false, false, false, true, false);
+
+ response = killBillClient.pluginOPTIONS(TEST_PLUGIN_NAME + "/" + TEST_PLUGIN_VALID_OPTIONS_PATH);
+ testAndResetAllMarkers(response, 230, TEST_PLUGIN_RESPONSE_BYTES, false, false, false, false, false, true);
+ }
+
+ private void testAndResetAllMarkers(@Nullable final Response response, final int responseCode, @Nullable final byte[] responseBytes, final boolean get, final boolean head,
+ final boolean post, final boolean put, final boolean delete, final boolean options) throws IOException {
+ if (responseCode == 404 || responseCode == 204) {
+ Assert.assertNull(response);
+ } else {
+ Assert.assertNotNull(response);
+ Assert.assertEquals(response.getStatusCode(), responseCode);
+ if (responseBytes != null) {
+ Assert.assertEquals(response.getResponseBodyAsBytes(), responseBytes);
+ }
+ }
+
+ Assert.assertEquals(requestGETMarker.get(), get);
+ Assert.assertEquals(requestHEADMarker.get(), head);
+ Assert.assertEquals(requestPOSTMarker.get(), post);
+ Assert.assertEquals(requestPUTMarker.get(), put);
+ Assert.assertEquals(requestDELETEMarker.get(), delete);
+ Assert.assertEquals(requestOPTIONSMarker.get(), options);
+
+ resetAllMarkers();
+ }
+
+ private void resetAllMarkers() {
+ requestGETMarker.set(false);
+ requestHEADMarker.set(false);
+ requestPOSTMarker.set(false);
+ requestPUTMarker.set(false);
+ requestDELETEMarker.set(false);
+ requestOPTIONSMarker.set(false);
+ }
+
+ private void setupOSGIPlugin() {
+ ((DefaultServletRouter) servletRouter).registerServiceFromPath(TEST_PLUGIN_NAME, new HttpServlet() {
+ @Override
+ protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ if (("/" + TEST_PLUGIN_VALID_GET_PATH).equals(req.getPathInfo())) {
+ requestGETMarker.set(true);
+ resp.getOutputStream().write(TEST_PLUGIN_RESPONSE_BYTES);
+ resp.setStatus(230);
+ }
+ }
+
+ @Override
+ protected void doHead(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ if (("/" + TEST_PLUGIN_VALID_HEAD_PATH).equals(req.getPathInfo())) {
+ requestHEADMarker.set(true);
+ }
+ }
+
+ @Override
+ protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ if (("/" + TEST_PLUGIN_VALID_POST_PATH).equals(req.getPathInfo())) {
+ requestPOSTMarker.set(true);
+ resp.getOutputStream().write(TEST_PLUGIN_RESPONSE_BYTES);
+ resp.setStatus(230);
+ }
+ }
+
+ @Override
+ protected void doPut(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ if (("/" + TEST_PLUGIN_VALID_PUT_PATH).equals(req.getPathInfo())) {
+ requestPUTMarker.set(true);
+ resp.getOutputStream().write(TEST_PLUGIN_RESPONSE_BYTES);
+ resp.setStatus(230);
+ }
+ }
+
+ @Override
+ protected void doDelete(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ if (("/" + TEST_PLUGIN_VALID_DELETE_PATH).equals(req.getPathInfo())) {
+ requestDELETEMarker.set(true);
+ resp.getOutputStream().write(TEST_PLUGIN_RESPONSE_BYTES);
+ resp.setStatus(230);
+ }
+ }
+
+ @Override
+ protected void doOptions(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ if (("/" + TEST_PLUGIN_VALID_OPTIONS_PATH).equals(req.getPathInfo())) {
+ requestOPTIONSMarker.set(true);
+ resp.getOutputStream().write(TEST_PLUGIN_RESPONSE_BYTES);
+ resp.setStatus(230);
+ }
+ }
+ });
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java b/server/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
new file mode 100644
index 0000000..7e08f17
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestPushNotification.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.jaxrs.json.NotificationJson;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.io.CharStreams;
+
+public class TestPushNotification extends TestJaxrsBase {
+
+ private CallbackServer callbackServer;
+
+ private static final int SERVER_PORT = 8087;
+ private static final String CALLBACK_ENDPPOINT = "/callmeback";
+
+ private volatile boolean callbackCompleted;
+ private volatile boolean callbackCompletedWithError;
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ callbackServer = new CallbackServer(this, SERVER_PORT, CALLBACK_ENDPPOINT);
+ callbackCompleted = false;
+ callbackCompletedWithError = false;
+ callbackServer.startServer();
+ }
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ callbackServer.stopServer();
+ }
+
+ private boolean waitForCallbacksToComplete() throws InterruptedException {
+ long remainingMs = 20000;
+ do {
+ if (callbackCompleted) {
+ break;
+ }
+ Thread.sleep(100);
+ remainingMs -= 100;
+ } while (remainingMs > 0);
+ return (remainingMs > 0);
+ }
+
+ public void retrieveAccountWithAsserts(final String accountId) {
+ try {
+ // Just check we can retrieve the account with the id from the callback
+ killBillClient.getAccount(UUID.fromString(accountId));
+ } catch (final Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testPushNotification() throws Exception {
+ // Register tenant for callback
+ killBillClient.registerCallbackNotificationForTenant("http://127.0.0.1:" + SERVER_PORT + CALLBACK_ENDPPOINT, createdBy, reason, comment);
+ // Create account to trigger a push notification
+ createAccount();
+
+ final boolean success = waitForCallbacksToComplete();
+ if (!success) {
+ Assert.fail("Fail to see push notification callbacks after 5 sec");
+ }
+
+ if (callbackCompletedWithError) {
+ Assert.fail("Assertion during callback failed...");
+ }
+ }
+
+ public void setCompleted(final boolean withError) {
+ callbackCompleted = true;
+ callbackCompletedWithError = withError;
+ }
+
+ public static class CallbackServer {
+
+ private final Server server;
+ private final String callbackEndpoint;
+ private final TestPushNotification test;
+
+ public CallbackServer(final TestPushNotification test, final int port, final String callbackEndpoint) {
+ this.callbackEndpoint = callbackEndpoint;
+ this.test = test;
+ this.server = new Server(port);
+ }
+
+ public void startServer() throws Exception {
+ final ServletContextHandler context = new ServletContextHandler();
+ context.setContextPath("/");
+ server.setHandler(context);
+ context.addServlet(new ServletHolder(new CallmebackServlet(test, 1)), callbackEndpoint);
+ server.start();
+ }
+
+ public void stopServer() throws Exception {
+ server.stop();
+ }
+ }
+
+ public static class CallmebackServlet extends HttpServlet {
+
+ private static final long serialVersionUID = -5181211514918217301L;
+
+ private static final Logger log = LoggerFactory.getLogger(CallmebackServlet.class);
+
+ private final int expectedNbCalls;
+ private final AtomicInteger receivedCalls;
+ private final TestPushNotification test;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ private boolean withError;
+
+ public CallmebackServlet(final TestPushNotification test, final int expectedNbCalls) {
+ this.expectedNbCalls = expectedNbCalls;
+ this.test = test;
+ this.receivedCalls = new AtomicInteger(0);
+ this.withError = false;
+ }
+
+ @Override
+ protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
+ final int current = receivedCalls.incrementAndGet();
+
+ final String body = CharStreams.toString(new InputStreamReader(request.getInputStream(), "UTF-8"));
+
+ response.setContentType("application/json");
+ response.setStatus(HttpServletResponse.SC_OK);
+
+ log.info("Got body {}", body);
+
+ try {
+ final NotificationJson notification = objectMapper.readValue(body, NotificationJson.class);
+ Assert.assertEquals(notification.getEventType(), "ACCOUNT_CREATION");
+ Assert.assertEquals(notification.getObjectType(), "ACCOUNT");
+ Assert.assertNotNull(notification.getObjectId());
+ Assert.assertNotNull(notification.getAccountId());
+ Assert.assertEquals(notification.getObjectId(), notification.getAccountId());
+
+ test.retrieveAccountWithAsserts(notification.getObjectId());
+ } catch (final AssertionError e) {
+ withError = true;
+ }
+
+ log.info("CallmebackServlet received {} calls , current = {}", current, body);
+ stopServerWhenComplete(current, withError);
+ }
+
+ private void stopServerWhenComplete(final int current, final boolean withError) {
+ if (current == expectedNbCalls) {
+ log.info("Excellent, we are done!");
+ test.setCompleted(withError);
+ }
+ }
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestSecurity.java b/server/src/test/java/org/killbill/billing/jaxrs/TestSecurity.java
new file mode 100644
index 0000000..4d32514
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestSecurity.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.HashSet;
+import java.util.List;
+
+import javax.annotation.Nullable;
+import javax.ws.rs.core.Response.Status;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.security.Permission;
+
+import com.google.common.collect.ImmutableSet;
+
+public class TestSecurity extends TestJaxrsBase {
+
+ @Test(groups = "slow")
+ public void testPermissions() throws Exception {
+ logout();
+
+ try {
+ killBillClient.getPermissions();
+ Assert.fail();
+ } catch (final KillBillClientException e) {
+ Assert.assertEquals(e.getResponse().getStatusCode(), Status.UNAUTHORIZED.getStatusCode());
+ }
+
+ // See src/test/resources/shiro.ini
+
+ final List<String> pierresPermissions = getPermissions("pierre", "password");
+ Assert.assertEquals(pierresPermissions.size(), 2);
+ Assert.assertEquals(new HashSet<String>(pierresPermissions), ImmutableSet.<String>of(Permission.INVOICE_CAN_CREDIT.toString(), Permission.INVOICE_CAN_ITEM_ADJUST.toString()));
+
+ final List<String> stephanesPermissions = getPermissions("stephane", "password");
+ Assert.assertEquals(stephanesPermissions.size(), 1);
+ Assert.assertEquals(new HashSet<String>(stephanesPermissions), ImmutableSet.<String>of(Permission.PAYMENT_CAN_REFUND.toString()));
+ }
+
+ private List<String> getPermissions(@Nullable final String username, @Nullable final String password) throws Exception {
+ login(username, password);
+ return killBillClient.getPermissions();
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jaxrs/TestTag.java b/server/src/test/java/org/killbill/billing/jaxrs/TestTag.java
new file mode 100644
index 0000000..f7e928a
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jaxrs/TestTag.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jaxrs;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Tag;
+import org.killbill.billing.client.model.TagDefinition;
+import org.killbill.billing.client.model.Tags;
+import org.killbill.billing.util.tag.ControlTag;
+import org.killbill.billing.util.tag.ControlTagType;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.fail;
+
+public class TestTag extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Cannot add badly formatted TagDefinition")
+ public void testTagErrorHandling() throws Exception {
+ final TagDefinition[] tagDefinitions = {new TagDefinition(null, false, null, null, null),
+ new TagDefinition(null, false, "something", null, null),
+ new TagDefinition(null, false, null, "something", null)};
+
+ for (final TagDefinition tagDefinition : tagDefinitions) {
+ try {
+ killBillClient.createTagDefinition(tagDefinition, createdBy, reason, comment);
+ fail();
+ } catch (final KillBillClientException e) {
+ }
+ }
+ }
+
+ @Test(groups = "slow", description = "Can create a TagDefinition")
+ public void testTagDefinitionOk() throws Exception {
+ final TagDefinition input = new TagDefinition(null, false, "blue", "relaxing color", ImmutableList.<ObjectType>of());
+
+ final TagDefinition objFromJson = killBillClient.createTagDefinition(input, createdBy, reason, comment);
+ assertNotNull(objFromJson);
+ assertEquals(objFromJson.getName(), input.getName());
+ assertEquals(objFromJson.getDescription(), input.getDescription());
+ }
+
+ @Test(groups = "slow", description = "Can create and delete TagDefinitions")
+ public void testMultipleTagDefinitionOk() throws Exception {
+ List<TagDefinition> objFromJson = killBillClient.getTagDefinitions();
+ final int sizeSystemTag = objFromJson.isEmpty() ? 0 : objFromJson.size();
+
+ final TagDefinition inputBlue = new TagDefinition(null, false, "blue", "relaxing color", ImmutableList.<ObjectType>of());
+ killBillClient.createTagDefinition(inputBlue, createdBy, reason, comment);
+
+ final TagDefinition inputRed = new TagDefinition(null, false, "red", "hot color", ImmutableList.<ObjectType>of());
+ killBillClient.createTagDefinition(inputRed, createdBy, reason, comment);
+
+ final TagDefinition inputYellow = new TagDefinition(null, false, "yellow", "vibrant color", ImmutableList.<ObjectType>of());
+ killBillClient.createTagDefinition(inputYellow, createdBy, reason, comment);
+
+ final TagDefinition inputGreen = new TagDefinition(null, false, "green", "super relaxing color", ImmutableList.<ObjectType>of());
+ killBillClient.createTagDefinition(inputGreen, createdBy, reason, comment);
+
+ objFromJson = killBillClient.getTagDefinitions();
+ assertNotNull(objFromJson);
+ assertEquals(objFromJson.size(), 4 + sizeSystemTag);
+
+ killBillClient.deleteTagDefinition(objFromJson.get(0).getId(), createdBy, reason, comment);
+
+ objFromJson = killBillClient.getTagDefinitions();
+ assertNotNull(objFromJson);
+ assertEquals(objFromJson.size(), 3 + sizeSystemTag);
+ }
+
+ @Test(groups = "slow", description = "Can search system tags")
+ public void testSystemTagsPagination() throws Exception {
+ final Account account = createAccount();
+ for (final ControlTagType controlTagType : ControlTagType.values()) {
+ killBillClient.createAccountTag(account.getAccountId(), controlTagType.getId(), createdBy, reason, comment);
+ }
+
+ final Tags allTags = killBillClient.getTags();
+ Assert.assertEquals(allTags.size(), ControlTagType.values().length);
+
+ for (final ControlTagType controlTagType : ControlTagType.values()) {
+ Assert.assertEquals(killBillClient.searchTags(controlTagType.toString()).size(), 1);
+ Assert.assertEquals(killBillClient.searchTags(controlTagType.getDescription()).size(), 1);
+ }
+ }
+
+ @Test(groups = "slow", description = "Can paginate through all tags")
+ public void testTagsPagination() throws Exception {
+ final Account account = createAccount();
+ for (int i = 0; i < 5; i++) {
+ final TagDefinition tagDefinition = new TagDefinition(null, false, UUID.randomUUID().toString().substring(0, 5), UUID.randomUUID().toString(), ImmutableList.<ObjectType>of(ObjectType.ACCOUNT));
+ final UUID tagDefinitionId = killBillClient.createTagDefinition(tagDefinition, createdBy, reason, comment).getId();
+ killBillClient.createAccountTag(account.getAccountId(), tagDefinitionId, createdBy, reason, comment);
+ }
+
+ final Tags allTags = killBillClient.getTags();
+ Assert.assertEquals(allTags.size(), 5);
+
+ Tags page = killBillClient.getTags(0L, 1L);
+ for (int i = 0; i < 5; i++) {
+ Assert.assertNotNull(page);
+ Assert.assertEquals(page.size(), 1);
+ Assert.assertEquals(page.get(0), allTags.get(i));
+ page = page.getNext();
+ }
+ Assert.assertNull(page);
+
+ for (final Tag tag : allTags) {
+ doSearchTag(UUID.randomUUID().toString(), null);
+ doSearchTag(tag.getTagId().toString(), tag);
+ doSearchTag(tag.getTagDefinitionName(), tag);
+
+ final TagDefinition tagDefinition = killBillClient.getTagDefinition(tag.getTagDefinitionId());
+ doSearchTag(tagDefinition.getDescription(), tag);
+ }
+
+ final Tags tags = killBillClient.searchTags(ObjectType.ACCOUNT.toString());
+ Assert.assertEquals(tags.size(), 5);
+ Assert.assertEquals(tags.getPaginationCurrentOffset(), 0);
+ Assert.assertEquals(tags.getPaginationTotalNbRecords(), 5);
+ Assert.assertEquals(tags.getPaginationMaxNbRecords(), 5);
+ }
+
+ private void doSearchTag(final String searchKey, @Nullable final Tag expectedTag) throws KillBillClientException {
+ final Tags tags = killBillClient.searchTags(searchKey);
+ if (expectedTag == null) {
+ Assert.assertTrue(tags.isEmpty());
+ Assert.assertEquals(tags.getPaginationCurrentOffset(), 0);
+ Assert.assertEquals(tags.getPaginationTotalNbRecords(), 0);
+ Assert.assertEquals(tags.getPaginationMaxNbRecords(), 5);
+ } else {
+ Assert.assertEquals(tags.size(), 1);
+ Assert.assertEquals(tags.get(0), expectedTag);
+ Assert.assertEquals(tags.getPaginationCurrentOffset(), 0);
+ Assert.assertEquals(tags.getPaginationTotalNbRecords(), 1);
+ Assert.assertEquals(tags.getPaginationMaxNbRecords(), 5);
+ }
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jetty/HttpServer.java b/server/src/test/java/org/killbill/billing/jetty/HttpServer.java
new file mode 100644
index 0000000..86a8813
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jetty/HttpServer.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jetty;
+
+import java.lang.management.ManagementFactory;
+import java.util.EnumSet;
+import java.util.EventListener;
+import java.util.Map;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.management.MBeanServer;
+import javax.servlet.DispatcherType;
+
+import org.eclipse.jetty.jmx.MBeanContainer;
+import org.eclipse.jetty.server.NCSARequestLog;
+import org.eclipse.jetty.server.RequestLog;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.server.handler.RequestLogHandler;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.xml.XmlConfiguration;
+import org.killbill.commons.skeleton.listeners.JULServletContextListener;
+
+import com.google.common.base.Preconditions;
+import com.google.common.io.Resources;
+import com.google.inject.servlet.GuiceFilter;
+
+/**
+ * Embed Jetty
+ */
+public class HttpServer {
+
+ private final Server server;
+
+ public HttpServer() {
+ this.server = new Server();
+ server.setSendServerVersion(false);
+ }
+
+ public HttpServer(final String jettyXml) throws Exception {
+ this();
+ configure(jettyXml);
+ }
+
+ public void configure(final String jettyXml) throws Exception {
+ final XmlConfiguration configuration = new XmlConfiguration(Resources.getResource(jettyXml));
+ configuration.configure(server);
+ }
+
+ public void configure(final HttpServerConfig config, final Iterable<EventListener> eventListeners, final Map<FilterHolder, String> filterHolders) {
+ server.setStopAtShutdown(true);
+
+ // Setup JMX
+ configureJMX(ManagementFactory.getPlatformMBeanServer());
+
+ // Configure main connector
+ configureMainConnector(config.isJettyStatsOn(), config.getServerHost(), config.getServerPort());
+
+ // Configure SSL, if enabled
+ if (config.isSSLEnabled()) {
+ configureSslConnector(config.isJettyStatsOn(), config.getServerSslPort(), config.getSSLkeystoreLocation(), config.getSSLkeystorePassword());
+ }
+
+ // Configure the thread pool
+ configureThreadPool(config);
+
+ // Configure handlers
+ final HandlerCollection handlers = new HandlerCollection();
+ final ServletContextHandler servletContextHandler = createServletContextHandler(config.getResourceBase(), eventListeners, filterHolders);
+ handlers.addHandler(servletContextHandler);
+ final RequestLogHandler logHandler = createLogHandler(config);
+ handlers.addHandler(logHandler);
+ final HandlerList rootHandlers = new HandlerList();
+ rootHandlers.addHandler(handlers);
+ server.setHandler(rootHandlers);
+ }
+
+ @PostConstruct
+ public void start() throws Exception {
+ server.start();
+ Preconditions.checkState(server.isRunning(), "server is not running");
+ }
+
+ @PreDestroy
+ public void stop() throws Exception {
+ server.stop();
+ }
+
+ private void configureJMX(final MBeanServer mbeanServer) {
+ final MBeanContainer mbContainer = new MBeanContainer(mbeanServer);
+ mbContainer.addBean(Log.getLogger(HttpServer.class));
+ server.addBean(mbContainer);
+ }
+
+ private void configureMainConnector(final boolean isStatsOn, final String localIp, final int localPort) {
+ final SelectChannelConnector connector = new SelectChannelConnector();
+ connector.setName("http");
+ connector.setStatsOn(isStatsOn);
+ connector.setHost(localIp);
+ connector.setPort(localPort);
+ server.addConnector(connector);
+ }
+
+ private void configureSslConnector(final boolean isStatsOn, final int localSslPort, final String sslKeyStorePath, final String sslKeyStorePassword) {
+ final SslSelectChannelConnector sslConnector = new SslSelectChannelConnector();
+ sslConnector.setName("https");
+ sslConnector.setStatsOn(isStatsOn);
+ sslConnector.setPort(localSslPort);
+ final SslContextFactory sslContextFactory = sslConnector.getSslContextFactory();
+ sslContextFactory.setKeyStorePath(sslKeyStorePath);
+ sslContextFactory.setKeyStorePassword(sslKeyStorePassword);
+ server.addConnector(sslConnector);
+ }
+
+ private void configureThreadPool(final HttpServerConfig config) {
+ final QueuedThreadPool threadPool = new QueuedThreadPool(config.getMaxThreads());
+ threadPool.setMinThreads(config.getMinThreads());
+ threadPool.setName("http-worker");
+ server.setThreadPool(threadPool);
+ }
+
+ private ServletContextHandler createServletContextHandler(final String resourceBase, final Iterable<EventListener> eventListeners, final Map<FilterHolder, String> filterHolders) {
+ final ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.NO_SESSIONS);
+ context.setContextPath("/");
+
+ if (resourceBase != null) {
+ // Required if you want a webapp directory. See ContextHandler#getResource and http://docs.codehaus.org/display/JETTY/Embedding+Jetty
+ final String webapp = this.getClass().getClassLoader().getResource(resourceBase).toExternalForm();
+ context.setResourceBase(webapp);
+ }
+
+ // Jersey insists on using java.util.logging (JUL)
+ final EventListener listener = new JULServletContextListener();
+ context.addEventListener(listener);
+
+ for (final EventListener eventListener : eventListeners) {
+ context.addEventListener(eventListener);
+ }
+
+ for (final FilterHolder filterHolder : filterHolders.keySet()) {
+ context.addFilter(filterHolder, filterHolders.get(filterHolder), EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
+ }
+
+ // Make sure Guice filter all requests
+ final FilterHolder filterHolder = new FilterHolder(GuiceFilter.class);
+ context.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
+
+ // Backend servlet for Guice - never used
+ final ServletHolder sh = new ServletHolder(DefaultServlet.class);
+ context.addServlet(sh, "/*");
+
+ return context;
+ }
+
+ private RequestLogHandler createLogHandler(final HttpServerConfig config) {
+ final RequestLogHandler logHandler = new RequestLogHandler();
+
+ final RequestLog requestLog = new NCSARequestLog(config.getLogPath());
+ logHandler.setRequestLog(requestLog);
+
+ return logHandler;
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/jetty/HttpServerConfig.java b/server/src/test/java/org/killbill/billing/jetty/HttpServerConfig.java
new file mode 100644
index 0000000..6d0d580
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/jetty/HttpServerConfig.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.jetty;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.DefaultNull;
+
+public interface HttpServerConfig {
+
+ @Config("org.killbill.server.ip")
+ @Default("127.0.0.1")
+ String getServerHost();
+
+ @Config("org.killbill.server.port")
+ @Default("8080")
+ int getServerPort();
+
+ @Config("org.killbill.server.server.ssl.enabled")
+ @Default("false")
+ boolean isSSLEnabled();
+
+ @Config("org.killbill.server.server.ssl.port")
+ @Default("8443")
+ int getServerSslPort();
+
+ @Config("org.killbill.server.jetty.stats")
+ @Default("true")
+ boolean isJettyStatsOn();
+
+ @Config("org.killbill.server.jetty.ssl.keystore")
+ @DefaultNull
+ String getSSLkeystoreLocation();
+
+ @Config("org.killbill.server.jetty.ssl.keystore.password")
+ @DefaultNull
+ String getSSLkeystorePassword();
+
+ @Config("org.killbill.server.jetty.maxThreads")
+ @Default("2000")
+ int getMaxThreads();
+
+ @Config("org.killbill.server.jetty.minThreads")
+ @Default("2")
+ int getMinThreads();
+
+ @Config("org.killbill.server.jetty.logPath")
+ @Default(".logs")
+ String getLogPath();
+
+ @Config("org.killbill.server.jetty.resourceBase")
+ @DefaultNull
+ String getResourceBase();
+}
diff --git a/server/src/test/java/org/killbill/billing/server/dao/TestEmbeddedDBFactory.java b/server/src/test/java/org/killbill/billing/server/dao/TestEmbeddedDBFactory.java
new file mode 100644
index 0000000..05dde7d
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/server/dao/TestEmbeddedDBFactory.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.dao;
+
+import java.util.Properties;
+
+import org.killbill.billing.KillbillTestSuite;
+import org.killbill.billing.server.config.DaoConfig;
+import org.killbill.commons.embeddeddb.EmbeddedDB;
+import org.killbill.commons.embeddeddb.EmbeddedDB.DBEngine;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class TestEmbeddedDBFactory extends KillbillTestSuite {
+
+ @Test(groups = "fast")
+ public void testJdbcParser() throws Exception {
+ final EmbeddedDB mysqlEmbeddedDb = EmbeddedDBFactory.get(createDaoConfig("jdbc:mysql://127.0.0.1:3306/killbill", "root", "root"));
+ Assert.assertEquals(mysqlEmbeddedDb.getDBEngine(), DBEngine.MYSQL);
+ checkEmbeddedDb(mysqlEmbeddedDb);
+
+ final EmbeddedDB h2EmbeddedDb = EmbeddedDBFactory.get(createDaoConfig("jdbc:h2:file:killbill;MODE=MYSQL;DB_CLOSE_DELAY=-1;MVCC=true;DB_CLOSE_ON_EXIT=FALSE", "root", "root"));
+ Assert.assertEquals(h2EmbeddedDb.getDBEngine(), DBEngine.H2);
+ checkEmbeddedDb(h2EmbeddedDb);
+
+ final EmbeddedDB genericEmbeddedDb = EmbeddedDBFactory.get(createDaoConfig("jdbc:derby://localhost:1527/killbill;collation=TERRITORY_BASED:PRIMARY", "root", "root"));
+ Assert.assertEquals(genericEmbeddedDb.getDBEngine(), DBEngine.GENERIC);
+ checkEmbeddedDb(genericEmbeddedDb);
+ }
+
+ private void checkEmbeddedDb(final EmbeddedDB embeddedDb) {
+ Assert.assertEquals(embeddedDb.getDatabaseName(), "killbill");
+ Assert.assertEquals(embeddedDb.getUsername(), "root");
+ Assert.assertEquals(embeddedDb.getPassword(), "root");
+ }
+
+ private DaoConfig createDaoConfig(final String url, final String user, final String password) {
+ final Properties properties = new Properties();
+ properties.put("org.killbill.dao.url", url);
+ properties.put("org.killbill.dao.user", user);
+ properties.put("org.killbill.dao.password", password);
+ return new ConfigurationObjectFactory(properties).build(DaoConfig.class);
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcRealm.java b/server/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcRealm.java
new file mode 100644
index 0000000..5e5e918
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcRealm.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.security;
+
+import java.util.UUID;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.mgt.DefaultSecurityManager;
+import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.subject.support.DelegatingSubject;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.jaxrs.TestJaxrsBase;
+import org.killbill.billing.tenant.api.DefaultTenant;
+import org.killbill.billing.tenant.dao.DefaultTenantDao;
+import org.killbill.billing.tenant.dao.TenantModelDao;
+import org.killbill.billing.util.dao.DefaultNonEntityDao;
+
+import com.jolbox.bonecp.BoneCPConfig;
+import com.jolbox.bonecp.BoneCPDataSource;
+
+public class TestKillbillJdbcRealm extends TestJaxrsBase {
+
+ private SecurityManager securityManager;
+ private DefaultTenant tenant;
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+
+ super.beforeMethod();
+
+ // Create the tenant
+ final DefaultTenantDao tenantDao = new DefaultTenantDao(dbi, clock, cacheControllerDispatcher, new DefaultNonEntityDao(dbi));
+ tenant = new DefaultTenant(UUID.randomUUID(), null, null, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), UUID.randomUUID().toString());
+ tenantDao.create(new TenantModelDao(tenant), internalCallContext);
+
+ // Setup the security manager
+ final BoneCPConfig dbConfig = new BoneCPConfig();
+ dbConfig.setJdbcUrl(helper.getJdbcConnectionString());
+ dbConfig.setUsername(helper.getUsername());
+ dbConfig.setPassword(helper.getPassword());
+
+ final KillbillJdbcRealm jdbcRealm;
+ jdbcRealm = new KillbillJdbcRealm();
+ jdbcRealm.setDataSource(new BoneCPDataSource(dbConfig));
+
+ securityManager = new DefaultSecurityManager(jdbcRealm);
+ }
+
+ @Test(groups = "slow")
+ public void testAuthentication() throws Exception {
+ final DelegatingSubject subject = new DelegatingSubject(securityManager);
+
+ // Good combo
+ final AuthenticationToken goodToken = new UsernamePasswordToken(tenant.getApiKey(), tenant.getApiSecret());
+ try {
+ securityManager.login(subject, goodToken);
+ Assert.assertTrue(true);
+ } catch (AuthenticationException e) {
+ Assert.fail();
+ }
+
+ // Bad login
+ final AuthenticationToken badPasswordToken = new UsernamePasswordToken(tenant.getApiKey(), tenant.getApiSecret() + "T");
+ try {
+ securityManager.login(subject, badPasswordToken);
+ Assert.fail();
+ } catch (AuthenticationException e) {
+ Assert.assertTrue(true);
+ }
+
+ // Bad password
+ final AuthenticationToken badLoginToken = new UsernamePasswordToken(tenant.getApiKey() + "U", tenant.getApiSecret());
+ try {
+ securityManager.login(subject, badLoginToken);
+ Assert.fail();
+ } catch (AuthenticationException e) {
+ Assert.assertTrue(true);
+ }
+ }
+}
diff --git a/server/src/test/java/org/killbill/billing/server/security/TestTenantFilter.java b/server/src/test/java/org/killbill/billing/server/security/TestTenantFilter.java
new file mode 100644
index 0000000..5e7c9ac
--- /dev/null
+++ b/server/src/test/java/org/killbill/billing/server/security/TestTenantFilter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.security;
+
+import javax.ws.rs.core.Response.Status;
+
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.client.KillBillClient;
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.KillBillHttpClient;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Tenant;
+import org.killbill.billing.jaxrs.TestJaxrsBase;
+
+public class TestTenantFilter extends TestJaxrsBase {
+
+ @AfterMethod(groups = "slow")
+ public void tearDown() throws Exception {
+ // Default credentials
+ loginTenant(DEFAULT_API_KEY, DEFAULT_API_SECRET);
+ }
+
+ @Test(groups = "slow")
+ public void testTenantShouldOnlySeeOwnAccount() throws Exception {
+ // Try to create an account without being logged-in
+ logoutTenant();
+ try {
+ killBillClient.createAccount(getAccount(), createdBy, reason, comment);
+ Assert.fail();
+ } catch (final KillBillClientException e) {
+ Assert.assertEquals(e.getResponse().getStatusCode(), Status.UNAUTHORIZED.getStatusCode());
+ }
+
+ // Create the tenant
+ final String apiKeyTenant1 = "pierre";
+ final String apiSecretTenant1 = "pierreIsFr3nch";
+ loginTenant(apiKeyTenant1, apiSecretTenant1);
+ final Tenant tenant1 = new Tenant();
+ tenant1.setApiKey(apiKeyTenant1);
+ tenant1.setApiSecret(apiSecretTenant1);
+ killBillClient.createTenant(tenant1, createdBy, reason, comment);
+
+ final Account account1 = createAccount();
+ Assert.assertEquals(killBillClient.getAccount(account1.getExternalKey()), account1);
+
+ logoutTenant();
+
+ // Create another tenant
+ final String apiKeyTenant2 = "stephane";
+ final String apiSecretTenant2 = "stephane1sAlsoFr3nch";
+ loginTenant(apiKeyTenant2, apiSecretTenant2);
+ final Tenant tenant2 = new Tenant();
+ tenant2.setApiKey(apiKeyTenant2);
+ tenant2.setApiSecret(apiSecretTenant2);
+ killBillClient.createTenant(tenant2, createdBy, reason, comment);
+
+ final Account account2 = createAccount();
+ Assert.assertEquals(killBillClient.getAccount(account2.getExternalKey()), account2);
+
+ // We should not be able to retrieve the first account as tenant2
+ Assert.assertNull(killBillClient.getAccount(account1.getExternalKey()));
+
+ // Same for tenant1 and account2
+ loginTenant(apiKeyTenant1, apiSecretTenant1);
+ Assert.assertNull(killBillClient.getAccount(account2.getExternalKey()));
+ }
+}
server/src/test/resources/killbill.properties 36(+17 -19)
diff --git a/server/src/test/resources/killbill.properties b/server/src/test/resources/killbill.properties
index 7edc08b..ca00d26 100644
--- a/server/src/test/resources/killbill.properties
+++ b/server/src/test/resources/killbill.properties
@@ -15,35 +15,33 @@
#
# Use killbill util test properties (DbiProvider/MysqltestingHelper) on the test side configured with killbill
-com.ning.billing.dbi.jdbc.url=jdbc:mysql://127.0.0.1:3306/killbill
+org.killbill.billing.dbi.jdbc.url=jdbc:mysql://127.0.0.1:3306/killbill
-killbill.catalog.uri=catalog-weapons.xml
+org.killbill.catalog.uri=catalog-weapons.xml
killbill.overdue.uri=overdue.xml
-killbill.payment.engine.events.off=false
-killbill.payment.retry.days=8,8,8
+org.killbill.payment.engine.events.off=false
+org.killbill.payment.retry.days=8,8,8
user.timezone=UTC
-com.ning.core.server.jetty.logPath=/var/tmp/.logs
-
-killbill.payment.engine.notifications.sleep=100
-killbill.invoice.engine.notifications.sleep=100
-killbill.billing.persistent.bus.main.sleep=100
-killbill.billing.persistent.bus.main.nbThreads=1
-killbill.billing.persistent.bus.main.claimed=1
-killbill.billing.persistent.bus.external.sleep=100
-killbill.billing.persistent.bus.external.nbThreads=1
-killbill.billing.persistent.bus.external.claimed=1
-killbill.billing.persistent.bus.external.tableName=bus_ext_events
-killbill.billing.persistent.bus.external.historyTableName=bus_ext_events_history
+org.killbill.core.server.jetty.logPath=/var/tmp/.logs
+
+org.killbill.persistent.bus.main.sleep=100
+org.killbill.persistent.bus.main.nbThreads=1
+org.killbill.persistent.bus.main.claimed=1
+org.killbill.persistent.bus.external.sleep=100
+org.killbill.persistent.bus.external.nbThreads=1
+org.killbill.persistent.bus.external.claimed=1
+org.killbill.persistent.bus.external.tableName=bus_ext_events
+org.killbill.persistent.bus.external.historyTableName=bus_ext_events_history
# Local DB
-#com.ning.billing.dbi.test.useLocalDb=true
+#org.killbill.billing.dbi.test.useLocalDb=true
-killbill.osgi.bundle.install.dir=/var/tmp/somethingthatdoesnotexist
+org.killbill.osgi.bundle.install.dir=/var/tmp/somethingthatdoesnotexist
# Speed up from the (more secure) default
-killbill.server.multitenant.hash_iterations=10
+org.killbill.server.multitenant.hash_iterations=10
ANTLR_USE_DIRECT_CLASS_LOADING=true
subscription/pom.xml 80(+30 -50)
diff --git a/subscription/pom.xml b/subscription/pom.xml
index 849be09..cd1b2c8 100644
--- a/subscription/pom.xml
+++ b/subscription/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-subscription</artifactId>
@@ -45,98 +45,78 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.antlr</groupId>
+ <artifactId>stringtemplate</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-catalog</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-queue</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>javax.inject</groupId>
- <artifactId>javax.inject</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.antlr</groupId>
- <artifactId>stringtemplate</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/alignment/BaseAligner.java b/subscription/src/main/java/org/killbill/billing/subscription/alignment/BaseAligner.java
new file mode 100644
index 0000000..57ce150
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/alignment/BaseAligner.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.alignment;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Duration;
+
+public class BaseAligner {
+
+ protected DateTime addDuration(final DateTime input, final Duration duration) {
+ return addOrRemoveDuration(input, duration, true);
+ }
+
+ protected DateTime removeDuration(final DateTime input, final Duration duration) {
+ return addOrRemoveDuration(input, duration, false);
+ }
+
+ private DateTime addOrRemoveDuration(final DateTime input, final Duration duration, boolean add) {
+ DateTime result = input;
+ switch (duration.getUnit()) {
+ case DAYS:
+ result = add ? result.plusDays(duration.getNumber()) : result.minusDays(duration.getNumber());
+ ;
+ break;
+
+ case MONTHS:
+ result = add ? result.plusMonths(duration.getNumber()) : result.minusMonths(duration.getNumber());
+ break;
+
+ case YEARS:
+ result = add ? result.plusYears(duration.getNumber()) : result.minusYears(duration.getNumber());
+ break;
+ case UNLIMITED:
+ default:
+ throw new RuntimeException("Trying to move to unlimited time period");
+ }
+ return result;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/alignment/MigrationPlanAligner.java b/subscription/src/main/java/org/killbill/billing/subscription/alignment/MigrationPlanAligner.java
new file mode 100644
index 0000000..ed23c49
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/alignment/MigrationPlanAligner.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.alignment;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.Duration;
+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.subscription.api.migration.SubscriptionBaseMigrationApi.SubscriptionMigrationCase;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApiException;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+
+import com.google.inject.Inject;
+
+public class MigrationPlanAligner extends BaseAligner {
+
+ private final CatalogService catalogService;
+
+ @Inject
+ public MigrationPlanAligner(final CatalogService catalogService) {
+ this.catalogService = catalogService;
+ }
+
+
+ public TimedMigration[] getEventsMigration(final SubscriptionMigrationCase[] input, final DateTime now)
+ throws SubscriptionBaseMigrationApiException {
+
+ try {
+ TimedMigration[] events;
+ final Plan plan0 = catalogService.getFullCatalog().findPlan(input[0].getPlanPhaseSpecifier().getProductName(),
+ input[0].getPlanPhaseSpecifier().getBillingPeriod(), input[0].getPlanPhaseSpecifier().getPriceListName(), now);
+
+ final Plan plan1 = (input.length > 1) ? catalogService.getFullCatalog().findPlan(input[1].getPlanPhaseSpecifier().getProductName(),
+ input[1].getPlanPhaseSpecifier().getBillingPeriod(), input[1].getPlanPhaseSpecifier().getPriceListName(), now) :
+ null;
+
+ DateTime migrationStartDate = input[0].getEffectiveDate();
+
+ if (isRegularMigratedSubscription(input)) {
+
+ events = getEventsOnRegularMigration(plan0,
+ getPlanPhase(plan0, input[0].getPlanPhaseSpecifier().getPhaseType()),
+ input[0].getPlanPhaseSpecifier().getPriceListName(),
+ migrationStartDate);
+
+ } else if (isRegularFutureCancelledMigratedSubscription(input)) {
+
+ events = getEventsOnFuturePlanCancelMigration(plan0,
+ getPlanPhase(plan0, input[0].getPlanPhaseSpecifier().getPhaseType()),
+ input[0].getPlanPhaseSpecifier().getPriceListName(),
+ migrationStartDate,
+ input[0].getCancelledDate());
+
+ } else if (isPhaseChangeMigratedSubscription(input)) {
+
+ final PhaseType curPhaseType = input[0].getPlanPhaseSpecifier().getPhaseType();
+ Duration curPhaseDuration = null;
+ for (final PlanPhase cur : plan0.getAllPhases()) {
+ if (cur.getPhaseType() == curPhaseType) {
+ curPhaseDuration = cur.getDuration();
+ break;
+ }
+ }
+ if (curPhaseDuration == null) {
+ throw new SubscriptionBaseMigrationApiException(String.format("Failed to compute current phase duration for plan %s and phase %s",
+ plan0.getName(), curPhaseType));
+ }
+
+ migrationStartDate = removeDuration(input[1].getEffectiveDate(), curPhaseDuration);
+ events = getEventsOnFuturePhaseChangeMigration(plan0,
+ getPlanPhase(plan0, input[0].getPlanPhaseSpecifier().getPhaseType()),
+ input[0].getPlanPhaseSpecifier().getPriceListName(),
+ migrationStartDate,
+ input[1].getEffectiveDate());
+
+ } else if (isPlanChangeMigratedSubscription(input)) {
+
+ events = getEventsOnFuturePlanChangeMigration(plan0,
+ getPlanPhase(plan0, input[0].getPlanPhaseSpecifier().getPhaseType()),
+ plan1,
+ getPlanPhase(plan1, input[1].getPlanPhaseSpecifier().getPhaseType()),
+ input[0].getPlanPhaseSpecifier().getPriceListName(),
+ migrationStartDate,
+ input[1].getEffectiveDate());
+
+ } else {
+ throw new SubscriptionBaseMigrationApiException("Unknown migration type");
+ }
+
+ return events;
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseMigrationApiException(e);
+ }
+ }
+
+ private TimedMigration[] getEventsOnRegularMigration(final Plan plan, final PlanPhase initialPhase, final String priceList, final DateTime effectiveDate) {
+ final TimedMigration[] result = new TimedMigration[1];
+ result[0] = new TimedMigration(effectiveDate, EventType.API_USER, ApiEventType.MIGRATE_ENTITLEMENT, plan, initialPhase, priceList);
+ return result;
+ }
+
+ private TimedMigration[] getEventsOnFuturePhaseChangeMigration(final Plan plan, final PlanPhase initialPhase, final String priceList, final DateTime effectiveDate, final DateTime effectiveDateForNextPhase)
+ throws SubscriptionBaseMigrationApiException {
+
+ final TimedMigration[] result = new TimedMigration[2];
+
+ result[0] = new TimedMigration(effectiveDate, EventType.API_USER, ApiEventType.MIGRATE_ENTITLEMENT, plan, initialPhase, priceList);
+ boolean foundCurrent = false;
+ PlanPhase nextPhase = null;
+ for (final PlanPhase cur : plan.getAllPhases()) {
+ if (cur == initialPhase) {
+ foundCurrent = true;
+ continue;
+ }
+ if (foundCurrent) {
+ nextPhase = cur;
+ }
+ }
+ if (nextPhase == null) {
+ throw new SubscriptionBaseMigrationApiException(String.format("Cannot find next phase for Plan %s and current Phase %s",
+ plan.getName(), initialPhase.getName()));
+ }
+ result[1] = new TimedMigration(effectiveDateForNextPhase, EventType.PHASE, null, plan, nextPhase, priceList);
+ return result;
+ }
+
+ private TimedMigration[] getEventsOnFuturePlanChangeMigration(final Plan currentPlan, final PlanPhase currentPhase, final Plan newPlan, final PlanPhase newPhase, final String priceList, final DateTime effectiveDate, final DateTime effectiveDateForChangePlan) {
+ final TimedMigration[] result = new TimedMigration[2];
+ result[0] = new TimedMigration(effectiveDate, EventType.API_USER, ApiEventType.MIGRATE_ENTITLEMENT, currentPlan, currentPhase, priceList);
+ result[1] = new TimedMigration(effectiveDateForChangePlan, EventType.API_USER, ApiEventType.CHANGE, newPlan, newPhase, priceList);
+ return result;
+ }
+
+ private TimedMigration[] getEventsOnFuturePlanCancelMigration(final Plan plan, final PlanPhase initialPhase, final String priceList, final DateTime effectiveDate, final DateTime effectiveDateForCancellation) {
+ final TimedMigration[] result = new TimedMigration[2];
+ result[0] = new TimedMigration(effectiveDate, EventType.API_USER, ApiEventType.MIGRATE_ENTITLEMENT, plan, initialPhase, priceList);
+ result[1] = new TimedMigration(effectiveDateForCancellation, EventType.API_USER, ApiEventType.CANCEL, null, null, null);
+ return result;
+ }
+
+
+ // STEPH should be in catalog
+ private PlanPhase getPlanPhase(final Plan plan, final PhaseType phaseType) throws SubscriptionBaseMigrationApiException {
+ for (final PlanPhase cur : plan.getAllPhases()) {
+ if (cur.getPhaseType() == phaseType) {
+ return cur;
+ }
+ }
+ throw new SubscriptionBaseMigrationApiException(String.format("Cannot find PlanPhase from Plan %s and type %s", plan.getName(), phaseType));
+ }
+
+ private boolean isRegularMigratedSubscription(final SubscriptionMigrationCase[] input) {
+ return (input.length == 1 && input[0].getCancelledDate() == null);
+ }
+
+ private boolean isRegularFutureCancelledMigratedSubscription(final SubscriptionMigrationCase[] input) {
+ return (input.length == 1 && input[0].getCancelledDate() != null);
+ }
+
+ private boolean isPhaseChangeMigratedSubscription(final SubscriptionMigrationCase[] input) {
+ if (input.length != 2) {
+ return false;
+ }
+ return (isSamePlan(input[0].getPlanPhaseSpecifier(), input[1].getPlanPhaseSpecifier()) &&
+ !isSamePhase(input[0].getPlanPhaseSpecifier(), input[1].getPlanPhaseSpecifier()));
+ }
+
+ private boolean isPlanChangeMigratedSubscription(final SubscriptionMigrationCase[] input) {
+ if (input.length != 2) {
+ return false;
+ }
+ return !isSamePlan(input[0].getPlanPhaseSpecifier(), input[1].getPlanPhaseSpecifier());
+ }
+
+ private boolean isSamePlan(final PlanPhaseSpecifier plan0, final PlanPhaseSpecifier plan1) {
+ if (plan0.getPriceListName().equals(plan1.getPriceListName()) &&
+ plan0.getProductName().equals(plan1.getProductName()) &&
+ plan0.getBillingPeriod() == plan1.getBillingPeriod()) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isSamePhase(final PlanPhaseSpecifier plan0, final PlanPhaseSpecifier plan1) {
+ if (plan0.getPhaseType() == plan1.getPhaseType()) {
+ return true;
+ }
+ return false;
+ }
+}
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
new file mode 100644
index 0000000..c219b7c
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/alignment/PlanAligner.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.alignment;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.Duration;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanAlignmentChange;
+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.ProductCategory;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+
+/**
+ * PlanAligner offers specific APIs to return the correct {@code TimedPhase} when creating, changing Plan or to compute
+ * next Phase on current Plan.
+ */
+public class PlanAligner extends BaseAligner {
+
+ private final CatalogService catalogService;
+
+ @Inject
+ public PlanAligner(final CatalogService catalogService) {
+ this.catalogService = catalogService;
+ }
+
+ private enum WhichPhase {
+ CURRENT,
+ NEXT
+ }
+
+ /**
+ * Returns the current and next phase for the subscription in creation
+ *
+ * @param subscription the subscription in creation (only the start date and the bundle start date are looked at)
+ * @param plan the current Plan
+ * @param initialPhase the initialPhase on which we should create that subscription. can be null
+ * @param priceList the priceList
+ * @param requestedDate the requested date (only used to load the catalog)
+ * @param effectiveDate the effective creation date (driven by the catalog policy, i.e. when the creation occurs)
+ * @return the current and next phases
+ * @throws CatalogApiException for catalog errors
+ * @throws org.killbill.billing.subscription.api.user.SubscriptionBaseApiException for subscription errors
+ */
+ public TimedPhase[] getCurrentAndNextTimedPhaseOnCreate(final DefaultSubscriptionBase subscription,
+ final Plan plan,
+ final PhaseType initialPhase,
+ final String priceList,
+ final DateTime requestedDate,
+ final DateTime effectiveDate) throws CatalogApiException, SubscriptionBaseApiException {
+ final List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription.getAlignStartDate(),
+ subscription.getBundleStartDate(),
+ plan,
+ initialPhase,
+ priceList,
+ requestedDate);
+ final TimedPhase[] result = new TimedPhase[2];
+ result[0] = getTimedPhase(timedPhases, effectiveDate, WhichPhase.CURRENT);
+ result[1] = getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
+ return result;
+ }
+
+ /**
+ * Returns current Phase for that Plan change
+ *
+ * @param subscription the subscription in change (only start date, bundle start date, current phase, plan and pricelist
+ * are looked at)
+ * @param plan the current Plan
+ * @param priceList the priceList on which we should change that subscription.
+ * @param requestedDate the requested date
+ * @param effectiveDate the effective change date (driven by the catalog policy, i.e. when the change occurs)
+ * @return the current phase
+ * @throws CatalogApiException for catalog errors
+ * @throws org.killbill.billing.subscription.api.user.SubscriptionBaseApiException for subscription errors
+ */
+ public TimedPhase getCurrentTimedPhaseOnChange(final DefaultSubscriptionBase subscription,
+ final Plan plan,
+ final String priceList,
+ final DateTime requestedDate,
+ final DateTime effectiveDate) throws CatalogApiException, SubscriptionBaseApiException {
+ return getTimedPhaseOnChange(subscription, plan, priceList, requestedDate, effectiveDate, WhichPhase.CURRENT);
+ }
+
+ /**
+ * Returns next Phase for that Plan change
+ *
+ * @param subscription the subscription in change (only start date, bundle start date, current phase, plan and pricelist
+ * are looked at)
+ * @param plan the current Plan
+ * @param priceList the priceList on which we should change that subscription.
+ * @param requestedDate the requested date
+ * @param effectiveDate the effective change date (driven by the catalog policy, i.e. when the change occurs)
+ * @return the next phase
+ * @throws CatalogApiException for catalog errors
+ * @throws org.killbill.billing.subscription.api.user.SubscriptionBaseApiException for subscription errors
+ */
+ public TimedPhase getNextTimedPhaseOnChange(final DefaultSubscriptionBase subscription,
+ final Plan plan,
+ final String priceList,
+ final DateTime requestedDate,
+ final DateTime effectiveDate) throws CatalogApiException, SubscriptionBaseApiException {
+ return getTimedPhaseOnChange(subscription, plan, priceList, requestedDate, effectiveDate, WhichPhase.NEXT);
+ }
+
+ /**
+ * Returns next Phase for that SubscriptionBase at a point in time
+ *
+ * @param subscription the subscription for which we need to compute the next Phase event
+ * @param requestedDate the requested date
+ * @param effectiveDate the date at which we look to compute that event. effective needs to be after last Plan change or initial Plan
+ * @return the next phase
+ */
+ public TimedPhase getNextTimedPhase(final DefaultSubscriptionBase subscription, final DateTime requestedDate, final DateTime effectiveDate) {
+ try {
+ final SubscriptionBaseTransitionData lastPlanTransition = subscription.getInitialTransitionForCurrentPlan();
+ if (effectiveDate.isBefore(lastPlanTransition.getEffectiveTransitionTime())) {
+ throw new SubscriptionBaseError(String.format("Cannot specify an effectiveDate prior to last Plan Change, subscription = %s, effectiveDate = %s",
+ subscription.getId(), effectiveDate));
+ }
+
+ switch (lastPlanTransition.getTransitionType()) {
+ // If we never had any Plan change, borrow the logic for createPlan alignment
+ case MIGRATE_ENTITLEMENT:
+ case CREATE:
+ case RE_CREATE:
+ case TRANSFER:
+ final List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription.getAlignStartDate(),
+ subscription.getBundleStartDate(),
+ lastPlanTransition.getNextPlan(),
+ lastPlanTransition.getNextPhase().getPhaseType(),
+ lastPlanTransition.getNextPriceList().getName(),
+ requestedDate);
+ return getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
+ // If we went through Plan changes, borrow the logic for changePlanWithRequestedDate alignment
+ case CHANGE:
+ return getTimedPhaseOnChange(subscription.getAlignStartDate(),
+ subscription.getBundleStartDate(),
+ lastPlanTransition.getPreviousPhase(),
+ lastPlanTransition.getPreviousPlan(),
+ lastPlanTransition.getPreviousPriceList().getName(),
+ lastPlanTransition.getNextPlan(),
+ lastPlanTransition.getNextPriceList().getName(),
+ requestedDate,
+ effectiveDate,
+ WhichPhase.NEXT);
+ default:
+ throw new SubscriptionBaseError(String.format("Unexpected initial transition %s for current plan %s on subscription %s",
+ lastPlanTransition.getTransitionType(), subscription.getCurrentPlan(), subscription.getId()));
+ }
+ } catch (Exception /* SubscriptionBaseApiException, CatalogApiException */ e) {
+ throw new SubscriptionBaseError(String.format("Could not compute next phase change for subscription %s", subscription.getId()), e);
+ }
+ }
+
+ private List<TimedPhase> getTimedPhaseOnCreate(final DateTime subscriptionStartDate,
+ final DateTime bundleStartDate,
+ final Plan plan,
+ final PhaseType initialPhase,
+ final String priceList,
+ final DateTime requestedDate)
+ throws CatalogApiException, SubscriptionBaseApiException {
+ final Catalog catalog = catalogService.getFullCatalog();
+
+ final PlanSpecifier planSpecifier = new PlanSpecifier(plan.getProduct().getName(),
+ plan.getProduct().getCategory(),
+ plan.getBillingPeriod(),
+ priceList);
+
+ final DateTime planStartDate;
+ final PlanAlignmentCreate alignment = catalog.planCreateAlignment(planSpecifier, requestedDate);
+ switch (alignment) {
+ case START_OF_SUBSCRIPTION:
+ planStartDate = subscriptionStartDate;
+ break;
+ case START_OF_BUNDLE:
+ planStartDate = bundleStartDate;
+ break;
+ default:
+ throw new SubscriptionBaseError(String.format("Unknown PlanAlignmentCreate %s", alignment));
+ }
+
+ return getPhaseAlignments(plan, initialPhase, planStartDate);
+ }
+
+ private TimedPhase getTimedPhaseOnChange(final DefaultSubscriptionBase subscription,
+ final Plan nextPlan,
+ final String nextPriceList,
+ final DateTime requestedDate,
+ final DateTime effectiveDate,
+ final WhichPhase which) throws CatalogApiException, SubscriptionBaseApiException {
+ return getTimedPhaseOnChange(subscription.getAlignStartDate(),
+ subscription.getBundleStartDate(),
+ subscription.getCurrentPhase(),
+ subscription.getCurrentPlan(),
+ subscription.getCurrentPriceList().getName(),
+ nextPlan,
+ nextPriceList,
+ requestedDate,
+ effectiveDate,
+ which);
+ }
+
+ private TimedPhase getTimedPhaseOnChange(final DateTime subscriptionStartDate,
+ final DateTime bundleStartDate,
+ final PlanPhase currentPhase,
+ final Plan currentPlan,
+ final String currentPriceList,
+ final Plan nextPlan,
+ final String priceList,
+ final DateTime requestedDate,
+ final DateTime effectiveDate,
+ final WhichPhase which) throws CatalogApiException, SubscriptionBaseApiException {
+ final Catalog catalog = catalogService.getFullCatalog();
+ final ProductCategory currentCategory = currentPlan.getProduct().getCategory();
+ final PlanPhaseSpecifier fromPlanPhaseSpecifier = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
+ currentCategory,
+ currentPlan.getBillingPeriod(),
+ currentPriceList,
+ currentPhase.getPhaseType());
+
+ final PlanSpecifier toPlanSpecifier = new PlanSpecifier(nextPlan.getProduct().getName(),
+ nextPlan.getProduct().getCategory(),
+ nextPlan.getBillingPeriod(),
+ priceList);
+
+ final DateTime planStartDate;
+ final PlanAlignmentChange alignment = catalog.planChangeAlignment(fromPlanPhaseSpecifier, toPlanSpecifier, requestedDate);
+ switch (alignment) {
+ case START_OF_SUBSCRIPTION:
+ planStartDate = subscriptionStartDate;
+ break;
+ case START_OF_BUNDLE:
+ planStartDate = bundleStartDate;
+ break;
+ case CHANGE_OF_PLAN:
+ planStartDate = effectiveDate;
+ break;
+ case CHANGE_OF_PRICELIST:
+ throw new SubscriptionBaseError(String.format("Not implemented yet %s", alignment));
+ default:
+ throw new SubscriptionBaseError(String.format("Unknown PlanAlignmentChange %s", alignment));
+ }
+
+ final List<TimedPhase> timedPhases = getPhaseAlignments(nextPlan, null, planStartDate);
+ return getTimedPhase(timedPhases, effectiveDate, which);
+ }
+
+ private List<TimedPhase> getPhaseAlignments(final Plan plan, @Nullable final PhaseType initialPhase, final DateTime initialPhaseStartDate) throws SubscriptionBaseApiException {
+ if (plan == null) {
+ return Collections.emptyList();
+ }
+
+ final List<TimedPhase> result = new LinkedList<TimedPhase>();
+ DateTime curPhaseStart = (initialPhase == null) ? initialPhaseStartDate : null;
+ DateTime nextPhaseStart;
+ for (final PlanPhase cur : plan.getAllPhases()) {
+ // For create we can specify the phase so skip any phase until we reach initialPhase
+ if (curPhaseStart == null) {
+ if (initialPhase != cur.getPhaseType()) {
+ continue;
+ }
+ curPhaseStart = initialPhaseStartDate;
+ }
+
+ result.add(new TimedPhase(cur, curPhaseStart));
+
+ // STEPH check for duration null instead TimeUnit UNLIMITED
+ if (cur.getPhaseType() != PhaseType.EVERGREEN) {
+ final Duration curPhaseDuration = cur.getDuration();
+ nextPhaseStart = addDuration(curPhaseStart, curPhaseDuration);
+ if (nextPhaseStart == null) {
+ throw new SubscriptionBaseError(String.format("Unexpected non ending UNLIMITED phase for plan %s",
+ plan.getName()));
+ }
+ curPhaseStart = nextPhaseStart;
+ }
+ }
+
+ if (initialPhase != null && curPhaseStart == null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BAD_PHASE, initialPhase);
+ }
+
+ return result;
+ }
+
+ // STEPH check for non evergreen Plans and what happens
+ private TimedPhase getTimedPhase(final List<TimedPhase> timedPhases, final DateTime effectiveDate, final WhichPhase which) {
+ TimedPhase cur = null;
+ TimedPhase next = null;
+ for (final TimedPhase phase : timedPhases) {
+ if (phase.getStartPhase().isAfter(effectiveDate)) {
+ next = phase;
+ break;
+ }
+ cur = phase;
+ }
+
+ switch (which) {
+ case CURRENT:
+ return cur;
+ case NEXT:
+ return next;
+ default:
+ throw new SubscriptionBaseError(String.format("Unexpected %s TimedPhase", which));
+ }
+ }
+
+
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/alignment/TimedMigration.java b/subscription/src/main/java/org/killbill/billing/subscription/alignment/TimedMigration.java
new file mode 100644
index 0000000..a35e745
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/alignment/TimedMigration.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.alignment;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+
+public class TimedMigration {
+
+ private final DateTime eventTime;
+ private final EventType eventType;
+ private final ApiEventType apiEventType;
+ private final Plan plan;
+ private final PlanPhase phase;
+ private final String priceList;
+
+ public TimedMigration(final DateTime eventTime, final EventType eventType, final ApiEventType apiEventType,
+ final Plan plan, final PlanPhase phase, final String priceList) {
+ this.eventTime = eventTime;
+ this.eventType = eventType;
+ this.apiEventType = apiEventType;
+ this.plan = plan;
+ this.phase = phase;
+ this.priceList = priceList;
+ }
+
+ public DateTime getEventTime() {
+ return eventTime;
+ }
+
+ public EventType getEventType() {
+ return eventType;
+ }
+
+ public ApiEventType getApiEventType() {
+ return apiEventType;
+ }
+
+ public Plan getPlan() {
+ return plan;
+ }
+
+ public PlanPhase getPhase() {
+ return phase;
+ }
+
+ public String getPriceList() {
+ return priceList;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TimedMigration");
+ sb.append("{apiEventType=").append(apiEventType);
+ sb.append(", eventTime=").append(eventTime);
+ sb.append(", eventType=").append(eventType);
+ sb.append(", plan=").append(plan);
+ sb.append(", phase=").append(phase);
+ sb.append(", priceList='").append(priceList).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final TimedMigration that = (TimedMigration) o;
+
+ if (apiEventType != that.apiEventType) {
+ return false;
+ }
+ if (eventTime != null ? !eventTime.equals(that.eventTime) : that.eventTime != null) {
+ return false;
+ }
+ if (eventType != that.eventType) {
+ return false;
+ }
+ if (phase != null ? !phase.equals(that.phase) : that.phase != null) {
+ return false;
+ }
+ if (plan != null ? !plan.equals(that.plan) : that.plan != null) {
+ return false;
+ }
+ if (priceList != null ? !priceList.equals(that.priceList) : that.priceList != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = eventTime != null ? eventTime.hashCode() : 0;
+ result = 31 * result + (eventType != null ? eventType.hashCode() : 0);
+ result = 31 * result + (apiEventType != null ? apiEventType.hashCode() : 0);
+ result = 31 * result + (plan != null ? plan.hashCode() : 0);
+ result = 31 * result + (phase != null ? phase.hashCode() : 0);
+ result = 31 * result + (priceList != null ? priceList.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/alignment/TimedPhase.java b/subscription/src/main/java/org/killbill/billing/subscription/alignment/TimedPhase.java
new file mode 100644
index 0000000..98f07f9
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/alignment/TimedPhase.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.alignment;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.PlanPhase;
+
+public final class TimedPhase {
+ private final PlanPhase phase;
+ private final DateTime startPhase;
+
+ public TimedPhase(final PlanPhase phase, final DateTime startPhase) {
+ this.phase = phase;
+ this.startPhase = startPhase;
+ }
+
+ public PlanPhase getPhase() {
+ return phase;
+ }
+
+ public DateTime getStartPhase() {
+ return startPhase;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TimedPhase");
+ sb.append("{phase=").append(phase);
+ sb.append(", startPhase=").append(startPhase);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final TimedPhase phase1 = (TimedPhase) o;
+
+ if (phase != null ? !phase.equals(phase1.phase) : phase1.phase != null) {
+ return false;
+ }
+ if (startPhase != null ? !startPhase.equals(phase1.startPhase) : phase1.startPhase != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = phase != null ? phase.hashCode() : 0;
+ result = 31 * result + (startPhase != null ? startPhase.hashCode() : 0);
+ return result;
+ }
+}
+
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/migration/AccountMigrationData.java b/subscription/src/main/java/org/killbill/billing/subscription/api/migration/AccountMigrationData.java
new file mode 100644
index 0000000..2a5a0be
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/migration/AccountMigrationData.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.migration;
+
+import java.util.List;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+
+
+public class AccountMigrationData {
+
+ private final List<BundleMigrationData> data;
+
+ public AccountMigrationData(final List<BundleMigrationData> data) {
+ super();
+ this.data = data;
+ }
+
+ public List<BundleMigrationData> getData() {
+ return data;
+ }
+
+ public static class BundleMigrationData {
+
+ private final DefaultSubscriptionBaseBundle data;
+ private final List<SubscriptionMigrationData> subscriptions;
+
+ public BundleMigrationData(final DefaultSubscriptionBaseBundle data,
+ final List<SubscriptionMigrationData> subscriptions) {
+ super();
+ this.data = data;
+ this.subscriptions = subscriptions;
+ }
+
+ public DefaultSubscriptionBaseBundle getData() {
+ return data;
+ }
+
+ public List<SubscriptionMigrationData> getSubscriptions() {
+ return subscriptions;
+ }
+ }
+
+ public static class SubscriptionMigrationData {
+
+ private final DefaultSubscriptionBase data;
+ private final List<SubscriptionBaseEvent> initialEvents;
+
+ public SubscriptionMigrationData(final DefaultSubscriptionBase data,
+ final List<SubscriptionBaseEvent> initialEvents,
+ final DateTime ctd) {
+ super();
+ // Set CTD to subscription object from MIGRATION_BILLING event
+ final SubscriptionBuilder builder = new SubscriptionBuilder(data);
+ if (ctd != null) {
+ builder.setChargedThroughDate(ctd);
+ }
+ this.data = new DefaultSubscriptionBase(builder);
+ this.initialEvents = initialEvents;
+ }
+
+ public DefaultSubscriptionBase getData() {
+ return data;
+ }
+
+ public List<SubscriptionBaseEvent> getInitialEvents() {
+ return initialEvents;
+ }
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java
new file mode 100644
index 0000000..cd0c710
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/migration/DefaultSubscriptionBaseMigrationApi.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.migration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.clock.Clock;
+import org.killbill.billing.subscription.alignment.MigrationPlanAligner;
+import org.killbill.billing.subscription.alignment.TimedMigration;
+import org.killbill.billing.subscription.api.SubscriptionApiBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData.BundleMigrationData;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData.SubscriptionMigrationData;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.phase.PhaseEvent;
+import org.killbill.billing.subscription.events.phase.PhaseEventData;
+import org.killbill.billing.subscription.events.user.ApiEvent;
+import org.killbill.billing.subscription.events.user.ApiEventBuilder;
+import org.killbill.billing.subscription.events.user.ApiEventCancel;
+import org.killbill.billing.subscription.events.user.ApiEventChange;
+import org.killbill.billing.subscription.events.user.ApiEventMigrateBilling;
+import org.killbill.billing.subscription.events.user.ApiEventMigrateSubscription;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+
+public class DefaultSubscriptionBaseMigrationApi extends SubscriptionApiBase implements SubscriptionBaseMigrationApi {
+
+ private final MigrationPlanAligner migrationAligner;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public DefaultSubscriptionBaseMigrationApi(final MigrationPlanAligner migrationAligner,
+ final SubscriptionBaseApiService apiService,
+ final CatalogService catalogService,
+ final SubscriptionDao dao,
+ final Clock clock,
+ final InternalCallContextFactory internalCallContextFactory) {
+ super(dao, apiService, clock, catalogService);
+ this.migrationAligner = migrationAligner;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public void migrate(final AccountMigration toBeMigrated, final CallContext context)
+ throws SubscriptionBaseMigrationApiException {
+ final AccountMigrationData accountMigrationData = createAccountMigrationData(toBeMigrated, context);
+ dao.migrate(toBeMigrated.getAccountKey(), accountMigrationData, internalCallContextFactory.createInternalCallContext(toBeMigrated.getAccountKey(), context));
+ }
+
+ private AccountMigrationData createAccountMigrationData(final AccountMigration toBeMigrated, final CallContext context)
+ throws SubscriptionBaseMigrationApiException {
+ final UUID accountId = toBeMigrated.getAccountKey();
+ final DateTime now = clock.getUTCNow();
+
+ final List<BundleMigrationData> accountBundleData = new LinkedList<BundleMigrationData>();
+
+ for (final BundleMigration curBundle : toBeMigrated.getBundles()) {
+
+ final DefaultSubscriptionBaseBundle bundleData = new DefaultSubscriptionBaseBundle(curBundle.getBundleKey(), accountId, now, now, now, now);
+ final List<SubscriptionMigrationData> bundleSubscriptionData = new LinkedList<AccountMigrationData.SubscriptionMigrationData>();
+
+ final List<SubscriptionMigration> sortedSubscriptions = Lists.newArrayList(curBundle.getSubscriptions());
+ // Make sure we have first BASE or STANDALONE, then ADDON and for each category order by CED
+ Collections.sort(sortedSubscriptions, new Comparator<SubscriptionMigration>() {
+ @Override
+ public int compare(final SubscriptionMigration o1,
+ final SubscriptionMigration o2) {
+ if (o1.getCategory().equals(o2.getCategory())) {
+ return o1.getSubscriptionCases()[0].getEffectiveDate().compareTo(o2.getSubscriptionCases()[0].getEffectiveDate());
+ } else {
+ if (!o1.getCategory().name().equalsIgnoreCase("ADD_ON")) {
+ return -1;
+ } else if (o1.getCategory().name().equalsIgnoreCase("ADD_ON")) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+ });
+
+ DateTime bundleStartDate = null;
+ for (final SubscriptionMigration curSub : sortedSubscriptions) {
+ SubscriptionMigrationData data = null;
+ if (bundleStartDate == null) {
+ data = createInitialSubscription(bundleData.getId(), curSub.getCategory(), curSub.getSubscriptionCases(), now, curSub.getChargedThroughDate(), context);
+ bundleStartDate = data.getInitialEvents().get(0).getEffectiveDate();
+ } else {
+ data = createSubscriptionMigrationDataWithBundleDate(bundleData.getId(), curSub.getCategory(), curSub.getSubscriptionCases(), now,
+ bundleStartDate, curSub.getChargedThroughDate(), context);
+ }
+ if (data != null) {
+ bundleSubscriptionData.add(data);
+ }
+ }
+ final BundleMigrationData bundleMigrationData = new BundleMigrationData(bundleData, bundleSubscriptionData);
+ accountBundleData.add(bundleMigrationData);
+ }
+
+ return new AccountMigrationData(accountBundleData);
+ }
+
+ private SubscriptionMigrationData createInitialSubscription(final UUID bundleId, final ProductCategory productCategory,
+ final SubscriptionMigrationCase[] input, final DateTime now, final DateTime ctd, final CallContext context)
+ throws SubscriptionBaseMigrationApiException {
+ final TimedMigration[] events = migrationAligner.getEventsMigration(input, now);
+ final DateTime migrationStartDate = events[0].getEventTime();
+ final List<SubscriptionBaseEvent> emptyEvents = Collections.emptyList();
+ final DefaultSubscriptionBase defaultSubscriptionBase = createSubscriptionForApiUse(new SubscriptionBuilder()
+ .setId(UUID.randomUUID())
+ .setBundleId(bundleId)
+ .setCategory(productCategory)
+ .setBundleStartDate(migrationStartDate)
+ .setAlignStartDate(migrationStartDate),
+ emptyEvents);
+ return new SubscriptionMigrationData(defaultSubscriptionBase, toEvents(defaultSubscriptionBase, now, ctd, events, context), ctd);
+ }
+
+ private SubscriptionMigrationData createSubscriptionMigrationDataWithBundleDate(final UUID bundleId, final ProductCategory productCategory,
+ final SubscriptionMigrationCase[] input, final DateTime now, final DateTime bundleStartDate, final DateTime ctd, final CallContext context)
+ throws SubscriptionBaseMigrationApiException {
+ final TimedMigration[] events = migrationAligner.getEventsMigration(input, now);
+ final DateTime migrationStartDate = events[0].getEventTime();
+ final List<SubscriptionBaseEvent> emptyEvents = Collections.emptyList();
+ final DefaultSubscriptionBase defaultSubscriptionBase = createSubscriptionForApiUse(new SubscriptionBuilder()
+ .setId(UUID.randomUUID())
+ .setBundleId(bundleId)
+ .setCategory(productCategory)
+ .setBundleStartDate(bundleStartDate)
+ .setAlignStartDate(migrationStartDate),
+ emptyEvents);
+ return new SubscriptionMigrationData(defaultSubscriptionBase, toEvents(defaultSubscriptionBase, now, ctd, events, context), ctd);
+ }
+
+ private List<SubscriptionBaseEvent> toEvents(final DefaultSubscriptionBase defaultSubscriptionBase, final DateTime now, final DateTime ctd, final TimedMigration[] migrationEvents, final CallContext context) {
+
+
+ if (ctd == null) {
+ throw new SubscriptionBaseError(String.format("Could not create migration billing event ctd = %s", ctd));
+ }
+
+ final List<SubscriptionBaseEvent> events = new ArrayList<SubscriptionBaseEvent>(migrationEvents.length);
+
+ ApiEventMigrateBilling apiEventMigrateBilling = null;
+
+ // The first event date after the MIGRATE_ENTITLEMENT event
+ DateTime nextEventDate = null;
+
+ boolean isCancelledSubscriptionPriorOrAtCTD = false;
+
+ for (final TimedMigration cur : migrationEvents) {
+
+
+ final ApiEventBuilder builder = new ApiEventBuilder()
+ .setSubscriptionId(defaultSubscriptionBase.getId())
+ .setEventPlan((cur.getPlan() != null) ? cur.getPlan().getName() : null)
+ .setEventPlanPhase((cur.getPhase() != null) ? cur.getPhase().getName() : null)
+ .setEventPriceList(cur.getPriceList())
+ .setActiveVersion(defaultSubscriptionBase.getActiveVersion())
+ .setEffectiveDate(cur.getEventTime())
+ .setProcessedDate(now)
+ .setRequestedDate(now)
+ .setFromDisk(true);
+
+
+ if (cur.getEventType() == EventType.PHASE) {
+ nextEventDate = nextEventDate != null && nextEventDate.compareTo(cur.getEventTime()) < 0 ? nextEventDate : cur.getEventTime();
+ final PhaseEvent nextPhaseEvent = PhaseEventData.createNextPhaseEvent(cur.getPhase().getName(), defaultSubscriptionBase, now, cur.getEventTime());
+ events.add(nextPhaseEvent);
+
+
+ } else if (cur.getEventType() == EventType.API_USER) {
+
+ switch (cur.getApiEventType()) {
+ case MIGRATE_ENTITLEMENT:
+ ApiEventMigrateSubscription creationEvent = new ApiEventMigrateSubscription(builder);
+ events.add(creationEvent);
+ break;
+
+ case CHANGE:
+ nextEventDate = nextEventDate != null && nextEventDate.compareTo(cur.getEventTime()) < 0 ? nextEventDate : cur.getEventTime();
+ events.add(new ApiEventChange(builder));
+ break;
+ case CANCEL:
+ isCancelledSubscriptionPriorOrAtCTD = !cur.getEventTime().isAfter(ctd);
+ nextEventDate = nextEventDate != null && nextEventDate.compareTo(cur.getEventTime()) < 0 ? nextEventDate : cur.getEventTime();
+ events.add(new ApiEventCancel(builder));
+ break;
+ default:
+ throw new SubscriptionBaseError(String.format("Unexpected type of api migration event %s", cur.getApiEventType()));
+ }
+ } else {
+ throw new SubscriptionBaseError(String.format("Unexpected type of migration event %s", cur.getEventType()));
+ }
+
+ // create the MIGRATE_BILLING based on the current state of the last event.
+ if (!cur.getEventTime().isAfter(ctd)) {
+ builder.setEffectiveDate(ctd);
+ builder.setUuid(UUID.randomUUID());
+ apiEventMigrateBilling = new ApiEventMigrateBilling(builder);
+ }
+ }
+ // Always ADD MIGRATE BILLING which is constructed from latest state seen in the stream prior to CTD
+ if (apiEventMigrateBilling != null && !isCancelledSubscriptionPriorOrAtCTD) {
+ events.add(apiEventMigrateBilling);
+ }
+
+ Collections.sort(events, new Comparator<SubscriptionBaseEvent>() {
+ int compForApiType(final SubscriptionBaseEvent o1, final SubscriptionBaseEvent o2, final ApiEventType type) {
+ ApiEventType apiO1 = null;
+ if (o1.getType() == EventType.API_USER) {
+ apiO1 = ((ApiEvent) o1).getEventType();
+ }
+ ApiEventType apiO2 = null;
+ if (o2.getType() == EventType.API_USER) {
+ apiO2 = ((ApiEvent) o2).getEventType();
+ }
+ if (apiO1 != null && apiO1.equals(type)) {
+ return -1;
+ } else if (apiO2 != null && apiO2.equals(type)) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int compare(final SubscriptionBaseEvent o1, final SubscriptionBaseEvent o2) {
+
+ int comp = o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
+ if (comp == 0) {
+ comp = compForApiType(o1, o2, ApiEventType.MIGRATE_ENTITLEMENT);
+ }
+ if (comp == 0) {
+ comp = compForApiType(o1, o2, ApiEventType.MIGRATE_BILLING);
+ }
+ return comp;
+ }
+ });
+
+ return events;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionApiBase.java b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionApiBase.java
new file mode 100644
index 0000000..14a29fe
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionApiBase.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.clock.Clock;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+
+public class SubscriptionApiBase {
+
+ protected final SubscriptionDao dao;
+
+ protected final SubscriptionBaseApiService apiService;
+ protected final Clock clock;
+ protected final CatalogService catalogService;
+
+ public SubscriptionApiBase(final SubscriptionDao dao, final SubscriptionBaseApiService apiService, final Clock clock, final CatalogService catalogService) {
+ this.dao = dao;
+ this.apiService = apiService;
+ this.clock = clock;
+ this.catalogService = catalogService;
+ }
+
+ protected List<SubscriptionBase> createSubscriptionsForApiUse(final List<SubscriptionBase> internalSubscriptions) {
+ return new ArrayList<SubscriptionBase>(Collections2.transform(internalSubscriptions, new Function<SubscriptionBase, SubscriptionBase>() {
+ @Override
+ public SubscriptionBase apply(final SubscriptionBase subscription) {
+ return createSubscriptionForApiUse((DefaultSubscriptionBase) subscription);
+ }
+ }));
+ }
+
+ protected DefaultSubscriptionBase createSubscriptionForApiUse(final SubscriptionBase internalSubscription) {
+ return new DefaultSubscriptionBase((DefaultSubscriptionBase) internalSubscription, apiService, clock);
+ }
+
+ protected DefaultSubscriptionBase createSubscriptionForApiUse(SubscriptionBuilder builder, List<SubscriptionBaseEvent> events) {
+ final DefaultSubscriptionBase subscription = new DefaultSubscriptionBase(builder, apiService, clock);
+ if (events.size() > 0) {
+ subscription.rebuildTransitions(events, catalogService.getFullCatalog());
+ }
+ return subscription;
+ }
+}
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
new file mode 100644
index 0000000..2fdf041
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+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.PlanPhaseSpecifier;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.callcontext.InternalCallContext;
+
+public interface SubscriptionBaseApiService {
+
+ public DefaultSubscriptionBase createPlan(SubscriptionBuilder builder, Plan plan, PhaseType initialPhase,
+ String realPriceList, DateTime requestedDate, DateTime effectiveDate, DateTime processedDate,
+ CallContext context)
+ throws SubscriptionBaseApiException;
+
+ @Deprecated
+ public boolean recreatePlan(final DefaultSubscriptionBase subscription, final PlanPhaseSpecifier spec, final DateTime requestedDateWithMs, final CallContext context)
+ throws SubscriptionBaseApiException;
+
+ public boolean cancel(DefaultSubscriptionBase subscription, CallContext context)
+ throws SubscriptionBaseApiException;
+
+ public boolean cancelWithRequestedDate(DefaultSubscriptionBase subscription, DateTime requestedDate, CallContext context)
+ throws SubscriptionBaseApiException;
+
+ public boolean cancelWithPolicy(DefaultSubscriptionBase subscription, BillingActionPolicy policy, CallContext context)
+ throws SubscriptionBaseApiException;
+
+ public boolean uncancel(DefaultSubscriptionBase subscription, CallContext context)
+ throws SubscriptionBaseApiException;
+
+ // Return the effective date of the change
+ public DateTime changePlan(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
+ String priceList, CallContext context)
+ throws SubscriptionBaseApiException;
+
+ // Return the effective date of the change
+ public DateTime changePlanWithRequestedDate(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
+ String priceList, DateTime requestedDate, CallContext context)
+ throws SubscriptionBaseApiException;
+
+ // Return the effective date of the change
+ public DateTime changePlanWithPolicy(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
+ String priceList, BillingActionPolicy policy, CallContext context)
+ throws SubscriptionBaseApiException;
+
+ public int cancelAddOnsIfRequired(final DefaultSubscriptionBase baseSubscription, final DateTime effectiveDate, final InternalCallContext context);
+}
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
new file mode 100644
index 0000000..9435853
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.svcs;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.clock.Clock;
+import org.killbill.clock.DefaultClock;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
+import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun.DryRunChangeReason;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.subscription.api.SubscriptionApiBase;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionStatusDryRun;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.engine.addon.AddonUtils;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;
+
+public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implements SubscriptionBaseInternalApi {
+
+ private final Logger log = LoggerFactory.getLogger(DefaultSubscriptionInternalApi.class);
+
+ private final AddonUtils addonUtils;
+ private final NonEntityDao nonEntityDao;
+
+ @Inject
+ public DefaultSubscriptionInternalApi(final SubscriptionDao dao,
+ final DefaultSubscriptionBaseApiService apiService,
+ final Clock clock,
+ final CatalogService catalogService,
+ final AddonUtils addonUtils,
+ final NonEntityDao nonEntityDao) {
+ super(dao, apiService, clock, catalogService);
+ this.addonUtils = addonUtils;
+ this.nonEntityDao = nonEntityDao;
+ }
+
+ @Override
+ public SubscriptionBase createSubscription(final UUID bundleId, final PlanPhaseSpecifier spec, final DateTime requestedDateWithMs, final InternalCallContext context) throws SubscriptionBaseApiException {
+ try {
+ final String realPriceList = (spec.getPriceListName() == null) ? PriceListSet.DEFAULT_PRICELIST_NAME : spec.getPriceListName();
+ final DateTime now = clock.getUTCNow();
+ final DateTime requestedDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
+ if (requestedDate.isAfter(now)) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_REQUESTED_DATE, now.toString(), requestedDate.toString());
+ }
+ final DateTime effectiveDate = requestedDate;
+
+ final Catalog catalog = catalogService.getFullCatalog();
+ final Plan plan = catalog.findPlan(spec.getProductName(), spec.getBillingPeriod(), realPriceList, requestedDate);
+
+ final PlanPhase phase = plan.getAllPhases()[0];
+ if (phase == null) {
+ throw new SubscriptionBaseError(String.format("No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
+ spec.getProductName(), spec.getBillingPeriod().toString(), realPriceList));
+ }
+
+ final SubscriptionBaseBundle bundle = dao.getSubscriptionBundleFromId(bundleId, context);
+ if (bundle == null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BUNDLE, bundleId);
+ }
+
+ DateTime bundleStartDate = null;
+ final DefaultSubscriptionBase baseSubscription = (DefaultSubscriptionBase) dao.getBaseSubscription(bundleId, context);
+ switch (plan.getProduct().getCategory()) {
+ case BASE:
+ if (baseSubscription != null) {
+ if (baseSubscription.getState() == EntitlementState.ACTIVE) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundleId);
+ }
+ }
+ bundleStartDate = requestedDate;
+ break;
+ case ADD_ON:
+ if (baseSubscription == null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BP, bundleId);
+ }
+ if (effectiveDate.isBefore(baseSubscription.getStartDate())) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_REQUESTED_DATE, effectiveDate.toString(), baseSubscription.getStartDate().toString());
+ }
+ addonUtils.checkAddonCreationRights(baseSubscription, plan);
+ bundleStartDate = baseSubscription.getStartDate();
+ break;
+ case STANDALONE:
+ if (baseSubscription != null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundleId);
+ }
+ // Not really but we don't care, there is no alignment for STANDALONE subscriptions
+ bundleStartDate = requestedDate;
+ break;
+ default:
+ throw new SubscriptionBaseError(String.format("Can't create subscription of type %s",
+ plan.getProduct().getCategory().toString()));
+ }
+
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT);
+ return apiService.createPlan(new SubscriptionBuilder()
+ .setId(UUID.randomUUID())
+ .setBundleId(bundleId)
+ .setCategory(plan.getProduct().getCategory())
+ .setBundleStartDate(bundleStartDate)
+ .setAlignStartDate(effectiveDate),
+ plan, spec.getPhaseType(), realPriceList, requestedDate, effectiveDate, now, context.toCallContext(tenantId));
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+ }
+
+ @Override
+ public SubscriptionBaseBundle createBundleForAccount(final UUID accountId, final String bundleKey, final InternalCallContext context) throws SubscriptionBaseApiException {
+
+ final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(bundleKey, context);
+ final DateTime now = clock.getUTCNow();
+ final DateTime originalCreatedDate = existingBundles.size() > 0 ? existingBundles.get(0).getCreatedDate() : now;
+ final DefaultSubscriptionBaseBundle bundle = new DefaultSubscriptionBaseBundle(bundleKey, accountId, now, originalCreatedDate, now, now);
+ return dao.createSubscriptionBundle(bundle, context);
+ }
+
+ @Override
+ public List<SubscriptionBaseBundle> getBundlesForAccountAndKey(final UUID accountId, final String bundleKey, final InternalTenantContext context) throws SubscriptionBaseApiException {
+ final List<SubscriptionBaseBundle> bundlesForAccountAndKey = dao.getSubscriptionBundlesForAccountAndKey(accountId, bundleKey, context);
+ return bundlesForAccountAndKey;
+ }
+
+ @Override
+ public List<SubscriptionBaseBundle> getBundlesForAccount(final UUID accountId, final InternalTenantContext context) {
+ return dao.getSubscriptionBundleForAccount(accountId, context);
+ }
+
+ @Override
+ public List<SubscriptionBaseBundle> getBundlesForKey(final String bundleKey, final InternalTenantContext context) {
+ final List<SubscriptionBaseBundle> result = dao.getSubscriptionBundlesForKey(bundleKey, context);
+ return result;
+ }
+
+ @Override
+ public Pagination<SubscriptionBaseBundle> getBundles(final Long offset, final Long limit, final InternalTenantContext context) {
+ return getEntityPaginationNoException(limit,
+ new SourcePaginationBuilder<SubscriptionBundleModelDao, SubscriptionBaseApiException>() {
+ @Override
+ public Pagination<SubscriptionBundleModelDao> build() {
+ return dao.get(offset, limit, context);
+ }
+ },
+ new Function<SubscriptionBundleModelDao, SubscriptionBaseBundle>() {
+ @Override
+ public SubscriptionBaseBundle apply(final SubscriptionBundleModelDao bundleModelDao) {
+ return SubscriptionBundleModelDao.toSubscriptionbundle(bundleModelDao);
+ }
+ }
+ );
+ }
+
+ @Override
+ public Pagination<SubscriptionBaseBundle> searchBundles(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+ return getEntityPaginationNoException(limit,
+ new SourcePaginationBuilder<SubscriptionBundleModelDao, SubscriptionBaseApiException>() {
+ @Override
+ public Pagination<SubscriptionBundleModelDao> build() {
+ return dao.searchSubscriptionBundles(searchKey, offset, limit, context);
+ }
+ },
+ new Function<SubscriptionBundleModelDao, SubscriptionBaseBundle>() {
+ @Override
+ public SubscriptionBaseBundle apply(final SubscriptionBundleModelDao bundleModelDao) {
+ return SubscriptionBundleModelDao.toSubscriptionbundle(bundleModelDao);
+ }
+ }
+ );
+
+ }
+
+ @Override
+ public Iterable<UUID> getNonAOSubscriptionIdsForKey(final String bundleKey, final InternalTenantContext context) {
+ return dao.getNonAOSubscriptionIdsForKey(bundleKey, context);
+ }
+
+ public static SubscriptionBaseBundle getActiveBundleForKeyNotException(final List<SubscriptionBaseBundle> existingBundles, final SubscriptionDao dao, final Clock clock, final InternalTenantContext context) {
+ for (SubscriptionBaseBundle cur : existingBundles) {
+ final List<SubscriptionBase> subscriptions = dao.getSubscriptions(cur.getId(), context);
+ for (SubscriptionBase s : subscriptions) {
+ if (s.getCategory() == ProductCategory.ADD_ON) {
+ continue;
+ }
+ if (s.getEndDate() == null || s.getEndDate().compareTo(clock.getUTCNow()) > 0) {
+ return cur;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public List<SubscriptionBase> getSubscriptionsForBundle(UUID bundleId,
+ InternalTenantContext context) {
+ final List<SubscriptionBase> internalSubscriptions = dao.getSubscriptions(bundleId, context);
+ return createSubscriptionsForApiUse(internalSubscriptions);
+ }
+
+ @Override
+ public Map<UUID, List<SubscriptionBase>> getSubscriptionsForAccount(final InternalTenantContext context) {
+ final Map<UUID, List<SubscriptionBase>> internalSubscriptions = dao.getSubscriptionsForAccount(context);
+ final Map<UUID, List<SubscriptionBase>> result = new HashMap<UUID, List<SubscriptionBase>>();
+ for (final UUID bundleId : internalSubscriptions.keySet()) {
+ result.put(bundleId, createSubscriptionsForApiUse(internalSubscriptions.get(bundleId)));
+ }
+ return result;
+ }
+
+ @Override
+ public SubscriptionBase getBaseSubscription(UUID bundleId,
+ InternalTenantContext context) throws SubscriptionBaseApiException {
+ final SubscriptionBase result = dao.getBaseSubscription(bundleId, context);
+ if (result == null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId);
+ }
+ return createSubscriptionForApiUse(result);
+ }
+
+ @Override
+
+ public SubscriptionBase getSubscriptionFromId(UUID id,
+ InternalTenantContext context) throws SubscriptionBaseApiException {
+ final SubscriptionBase result = dao.getSubscriptionFromId(id, context);
+ if (result == null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_SUBSCRIPTION_ID, id);
+ }
+ return createSubscriptionForApiUse(result);
+ }
+
+ @Override
+ public SubscriptionBaseBundle getBundleFromId(final UUID id, final InternalTenantContext context) throws SubscriptionBaseApiException {
+ final SubscriptionBaseBundle result = dao.getSubscriptionBundleFromId(id, context);
+ if (result == null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_GET_INVALID_BUNDLE_ID, id.toString());
+ }
+ return result;
+ }
+
+ @Override
+ public UUID getAccountIdFromSubscriptionId(final UUID subscriptionId, final InternalTenantContext context) throws SubscriptionBaseApiException {
+ return dao.getAccountIdFromSubscriptionId(subscriptionId, context);
+ }
+
+ @Override
+ public void setChargedThroughDate(UUID subscriptionId,
+ DateTime chargedThruDate, InternalCallContext context) {
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) dao.getSubscriptionFromId(subscriptionId, context);
+ final SubscriptionBuilder builder = new SubscriptionBuilder(subscription)
+ .setChargedThroughDate(chargedThruDate);
+
+ dao.updateChargedThroughDate(new DefaultSubscriptionBase(builder), context);
+ }
+
+ @Override
+ public List<EffectiveSubscriptionInternalEvent> getAllTransitions(final SubscriptionBase subscription, final InternalTenantContext context) {
+ final List<SubscriptionBaseTransition> transitions = ((DefaultSubscriptionBase) subscription).getAllTransitions();
+ return convertEffectiveSubscriptionInternalEventFromSubscriptionTransitions(subscription, context, transitions);
+ }
+
+ @Override
+ public List<EffectiveSubscriptionInternalEvent> getBillingTransitions(final SubscriptionBase subscription, final InternalTenantContext context) {
+ final List<SubscriptionBaseTransition> transitions = ((DefaultSubscriptionBase) subscription).getBillingTransitions();
+ return convertEffectiveSubscriptionInternalEventFromSubscriptionTransitions(subscription, context, transitions);
+ }
+
+ @Override
+ public DateTime getNextBillingDate(final UUID accountId, final InternalTenantContext context) {
+ final List<SubscriptionBaseBundle> bundles = getBundlesForAccount(accountId, context);
+ DateTime result = null;
+ for (final SubscriptionBaseBundle bundle : bundles) {
+ final List<SubscriptionBase> subscriptions = getSubscriptionsForBundle(bundle.getId(), context);
+ for (final SubscriptionBase subscription : subscriptions) {
+ final DateTime chargedThruDate = subscription.getChargedThroughDate();
+ if (result == null ||
+ (chargedThruDate != null && chargedThruDate.isBefore(result))) {
+ result = subscription.getChargedThroughDate();
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(final UUID subscriptionId, @Nullable final String baseProductName, final DateTime requestedDate, final InternalTenantContext context) throws SubscriptionBaseApiException {
+ final SubscriptionBase subscription = dao.getSubscriptionFromId(subscriptionId, context);
+ if (subscription == null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_SUBSCRIPTION_ID, subscriptionId);
+ }
+ if (subscription.getCategory() != ProductCategory.BASE) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_DRY_RUN_NOT_BP);
+ }
+
+ final List<EntitlementAOStatusDryRun> result = new LinkedList<EntitlementAOStatusDryRun>();
+
+ final List<SubscriptionBase> bundleSubscriptions = dao.getSubscriptions(subscription.getBundleId(), context);
+ for (final SubscriptionBase cur : bundleSubscriptions) {
+ if (cur.getId().equals(subscriptionId)) {
+ continue;
+ }
+
+ // If ADDON is cancelled, skip
+ if (cur.getState() == EntitlementState.CANCELLED) {
+ continue;
+ }
+
+ final DryRunChangeReason reason;
+ // If baseProductName is null, it's a cancellation dry-run. In this case, return all addons, so they are cancelled
+ if (baseProductName != null && addonUtils.isAddonIncludedFromProdName(baseProductName, requestedDate, cur.getCurrentPlan())) {
+ reason = DryRunChangeReason.AO_INCLUDED_IN_NEW_PLAN;
+ } else if (baseProductName != null && addonUtils.isAddonAvailableFromProdName(baseProductName, requestedDate, cur.getCurrentPlan())) {
+ reason = DryRunChangeReason.AO_AVAILABLE_IN_NEW_PLAN;
+ } else {
+ reason = DryRunChangeReason.AO_NOT_AVAILABLE_IN_NEW_PLAN;
+ }
+ final EntitlementAOStatusDryRun status = new DefaultSubscriptionStatusDryRun(cur.getId(),
+ cur.getCurrentPlan().getProduct().getName(),
+ cur.getCurrentPhase().getPhaseType(),
+ cur.getCurrentPlan().getBillingPeriod(),
+ cur.getCurrentPriceList().getName(), reason);
+ result.add(status);
+ }
+ return result;
+ }
+
+ @Override
+ public void updateExternalKey(final UUID bundleId, final String newExternalKey, final InternalCallContext context) {
+ dao.updateBundleExternalKey(bundleId, newExternalKey, context);
+ }
+
+ private List<EffectiveSubscriptionInternalEvent> convertEffectiveSubscriptionInternalEventFromSubscriptionTransitions(final SubscriptionBase subscription,
+ final InternalTenantContext context, final List<SubscriptionBaseTransition> transitions) {
+ return ImmutableList.<EffectiveSubscriptionInternalEvent>copyOf(Collections2.transform(transitions, new Function<SubscriptionBaseTransition, EffectiveSubscriptionInternalEvent>() {
+ @Override
+ @Nullable
+ public EffectiveSubscriptionInternalEvent apply(@Nullable SubscriptionBaseTransition input) {
+ return new DefaultEffectiveSubscriptionEvent((SubscriptionBaseTransitionData) input, ((DefaultSubscriptionBase) subscription).getAlignStartDate(), null, context.getAccountRecordId(), context.getTenantRecordId());
+ }
+ }));
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultDeletedEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultDeletedEvent.java
new file mode 100644
index 0000000..506cd7a
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultDeletedEvent.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.DeletedEvent;
+
+public class DefaultDeletedEvent implements DeletedEvent {
+
+ private final UUID id;
+ private final DateTime effectiveDate;
+
+ public DefaultDeletedEvent(final UUID id, final DateTime effectiveDate) {
+ this.id = id;
+ this.effectiveDate = effectiveDate;
+ }
+
+ @Override
+ public UUID getEventId() {
+ return id;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultNewEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultNewEvent.java
new file mode 100644
index 0000000..79303a6
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultNewEvent.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.NewEvent;
+
+public class DefaultNewEvent implements NewEvent {
+
+ private final UUID subscriptionId;
+ private final PlanPhaseSpecifier spec;
+ private final DateTime requestedDate;
+ private final SubscriptionBaseTransitionType transitionType;
+
+ public DefaultNewEvent(final UUID subscriptionId, final PlanPhaseSpecifier spec, final DateTime requestedDate, final SubscriptionBaseTransitionType transitionType) {
+ this.subscriptionId = subscriptionId;
+ this.spec = spec;
+ this.requestedDate = requestedDate;
+ this.transitionType = transitionType;
+ }
+
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return spec;
+ }
+
+ @Override
+ public DateTime getRequestedDate() {
+ return requestedDate;
+ }
+
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return transitionType;
+ }
+
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultRepairSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultRepairSubscriptionEvent.java
new file mode 100644
index 0000000..5439175
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultRepairSubscriptionEvent.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.RepairSubscriptionInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultRepairSubscriptionEvent extends BusEventBase implements RepairSubscriptionInternalEvent {
+
+ private final UUID bundleId;
+ private final UUID accountId;
+ private final DateTime effectiveDate;
+
+
+ @JsonCreator
+ public DefaultRepairSubscriptionEvent(@JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("bundleId") final UUID bundleId,
+ @JsonProperty("effectiveDate") final DateTime effectiveDate,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.bundleId = bundleId;
+ this.accountId = accountId;
+ this.effectiveDate = effectiveDate;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.BUNDLE_REPAIR;
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((accountId == null) ? 0 : accountId.hashCode());
+ result = prime * result
+ + ((bundleId == null) ? 0 : bundleId.hashCode());
+ result = prime * result
+ + ((effectiveDate == null) ? 0 : effectiveDate.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final DefaultRepairSubscriptionEvent other = (DefaultRepairSubscriptionEvent) obj;
+ if (accountId == null) {
+ if (other.accountId != null) {
+ return false;
+ }
+ } else if (!accountId.equals(other.accountId)) {
+ return false;
+ }
+ if (bundleId == null) {
+ if (other.bundleId != null) {
+ return false;
+ }
+ } else if (!bundleId.equals(other.bundleId)) {
+ return false;
+ }
+ if (effectiveDate == null) {
+ if (other.effectiveDate != null) {
+ return false;
+ }
+ } else if (effectiveDate.compareTo(other.effectiveDate) != 0) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java
new file mode 100644
index 0000000..81447ed
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimeline.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.phase.PhaseEvent;
+import org.killbill.billing.subscription.events.user.ApiEvent;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+
+
+public class DefaultSubscriptionBaseTimeline implements SubscriptionBaseTimeline {
+
+ private final UUID id;
+ private final List<ExistingEvent> existingEvents;
+ private final List<NewEvent> newEvents;
+ private final List<DeletedEvent> deletedEvents;
+ private final long activeVersion;
+
+ public DefaultSubscriptionBaseTimeline(final UUID id, final long activeVersion) {
+ this.id = id;
+ this.activeVersion = activeVersion;
+ this.existingEvents = Collections.<SubscriptionBaseTimeline.ExistingEvent>emptyList();
+ this.deletedEvents = Collections.<SubscriptionBaseTimeline.DeletedEvent>emptyList();
+ this.newEvents = Collections.<SubscriptionBaseTimeline.NewEvent>emptyList();
+ }
+
+ public DefaultSubscriptionBaseTimeline(final SubscriptionBaseTimeline input) {
+ this.id = input.getId();
+ this.activeVersion = input.getActiveVersion();
+ this.existingEvents = (input.getExistingEvents() != null) ? new ArrayList<SubscriptionBaseTimeline.ExistingEvent>(input.getExistingEvents()) :
+ Collections.<SubscriptionBaseTimeline.ExistingEvent>emptyList();
+ sortExistingEvent(this.existingEvents);
+ this.deletedEvents = (input.getDeletedEvents() != null) ? new ArrayList<SubscriptionBaseTimeline.DeletedEvent>(input.getDeletedEvents()) :
+ Collections.<SubscriptionBaseTimeline.DeletedEvent>emptyList();
+ this.newEvents = (input.getNewEvents() != null) ? new ArrayList<SubscriptionBaseTimeline.NewEvent>(input.getNewEvents()) :
+ Collections.<SubscriptionBaseTimeline.NewEvent>emptyList();
+ sortNewEvent(this.newEvents);
+ }
+
+ // CTOR for returning events only
+ public DefaultSubscriptionBaseTimeline(final SubscriptionDataRepair input, final Catalog catalog) throws CatalogApiException {
+ this.id = input.getId();
+ this.existingEvents = toExistingEvents(catalog, input.getActiveVersion(), input.getCategory(), input.getEvents());
+ this.deletedEvents = null;
+ this.newEvents = null;
+ this.activeVersion = input.getActiveVersion();
+ }
+
+ private List<ExistingEvent> toExistingEvents(final Catalog catalog, final long activeVersion, final ProductCategory category, final List<SubscriptionBaseEvent> events)
+ throws CatalogApiException {
+
+ final List<ExistingEvent> result = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+
+ String prevProductName = null;
+ BillingPeriod prevBillingPeriod = null;
+ String prevPriceListName = null;
+ PhaseType prevPhaseType = null;
+
+ DateTime startDate = null;
+
+ for (final SubscriptionBaseEvent cur : events) {
+
+ // First active event is used to figure out which catalog version to use.
+ //startDate = (startDate == null && cur.getActiveVersion() == activeVersion) ? cur.getEffectiveDate() : startDate;
+
+ // STEPH that needs to be reviewed if we support multi version events
+ if (cur.getActiveVersion() != activeVersion || !cur.isActive()) {
+ continue;
+ }
+ startDate = (startDate == null) ? cur.getEffectiveDate() : startDate;
+
+
+ String productName = null;
+ BillingPeriod billingPeriod = null;
+ String priceListName = null;
+ PhaseType phaseType = null;
+ String planPhaseName = null;
+
+ ApiEventType apiType = null;
+ switch (cur.getType()) {
+ case PHASE:
+ final PhaseEvent phaseEV = (PhaseEvent) cur;
+ planPhaseName = phaseEV.getPhase();
+ phaseType = catalog.findPhase(phaseEV.getPhase(), cur.getEffectiveDate(), startDate).getPhaseType();
+ productName = prevProductName;
+ billingPeriod = catalog.findPhase(phaseEV.getPhase(), cur.getEffectiveDate(), startDate).getBillingPeriod();
+ priceListName = prevPriceListName;
+ break;
+
+ case API_USER:
+ final ApiEvent userEV = (ApiEvent) cur;
+ apiType = userEV.getEventType();
+ planPhaseName = userEV.getEventPlanPhase();
+ final Plan plan = (userEV.getEventPlan() != null) ? catalog.findPlan(userEV.getEventPlan(), cur.getRequestedDate(), startDate) : null;
+ phaseType = (userEV.getEventPlanPhase() != null) ? catalog.findPhase(userEV.getEventPlanPhase(), cur.getEffectiveDate(), startDate).getPhaseType() : prevPhaseType;
+ productName = (plan != null) ? plan.getProduct().getName() : prevProductName;
+ billingPeriod = (userEV.getEventPlanPhase() != null) ? catalog.findPhase(userEV.getEventPlanPhase(), cur.getEffectiveDate(), startDate).getBillingPeriod() : prevBillingPeriod;
+ priceListName = (userEV.getPriceList() != null) ? userEV.getPriceList() : prevPriceListName;
+ break;
+ }
+
+ final SubscriptionBaseTransitionType transitionType = SubscriptionBaseTransitionData.toSubscriptionTransitionType(cur.getType(), apiType);
+
+ final String planPhaseNameWithClosure = planPhaseName;
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(productName, category, billingPeriod, priceListName, phaseType);
+ result.add(new ExistingEvent() {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return transitionType;
+ }
+
+ @Override
+ public DateTime getRequestedDate() {
+ return cur.getRequestedDate();
+ }
+
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return spec;
+ }
+
+ @Override
+ public UUID getEventId() {
+ return cur.getId();
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return cur.getEffectiveDate();
+ }
+
+ @Override
+ public String getPlanPhaseName() {
+ return planPhaseNameWithClosure;
+ }
+ });
+
+ prevProductName = productName;
+ prevBillingPeriod = billingPeriod;
+ prevPriceListName = priceListName;
+ prevPhaseType = phaseType;
+
+ }
+ sortExistingEvent(result);
+ return result;
+ }
+
+
+ /*
+
+ private List<ExistingEvent> toExistingEvents(final Catalog catalog, final long processingVersion, final ProductCategory category, final List<SubscriptionBaseEvent> events, List<ExistingEvent> result)
+ throws CatalogApiException {
+
+
+ String prevProductName = null;
+ BillingPeriod prevBillingPeriod = null;
+ String prevPriceListName = null;
+ PhaseType prevPhaseType = null;
+
+ DateTime startDate = null;
+
+ for (final SubscriptionBaseEvent cur : events) {
+
+ if (processingVersion != cur.getActiveVersion()) {
+ continue;
+ }
+
+ // First active event is used to figure out which catalog version to use.
+ startDate = (startDate == null && cur.getActiveVersion() == processingVersion) ? cur.getEffectiveDate() : startDate;
+
+ String productName = null;
+ BillingPeriod billingPeriod = null;
+ String priceListName = null;
+ PhaseType phaseType = null;
+
+ ApiEventType apiType = null;
+ switch (cur.getType()) {
+ case PHASE:
+ PhaseEvent phaseEV = (PhaseEvent) cur;
+ phaseType = catalog.findPhase(phaseEV.getPhase(), cur.getEffectiveDate(), startDate).getPhaseType();
+ productName = prevProductName;
+ billingPeriod = prevBillingPeriod;
+ priceListName = prevPriceListName;
+ break;
+
+ case API_USER:
+ ApiEvent userEV = (ApiEvent) cur;
+ apiType = userEV.getEventType();
+ Plan plan = (userEV.getEventPlan() != null) ? catalog.findPlan(userEV.getEventPlan(), cur.getRequestedDate(), startDate) : null;
+ phaseType = (userEV.getEventPlanPhase() != null) ? catalog.findPhase(userEV.getEventPlanPhase(), cur.getEffectiveDate(), startDate).getPhaseType() : prevPhaseType;
+ productName = (plan != null) ? plan.getProduct().getName() : prevProductName;
+ billingPeriod = (plan != null) ? plan.getBillingPeriod() : prevBillingPeriod;
+ priceListName = (userEV.getPriceList() != null) ? userEV.getPriceList() : prevPriceListName;
+ break;
+ }
+
+ final SubscriptionBaseTransitionType transitionType = SubscriptionBaseTransitionData.toSubscriptionTransitionType(cur.getType(), apiType);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(productName, category, billingPeriod, priceListName, phaseType);
+ result.add(new ExistingEvent() {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return transitionType;
+ }
+ @Override
+ public DateTime getRequestedDate() {
+ return cur.getRequestedDate();
+ }
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return spec;
+ }
+ @Override
+ public UUID getEventId() {
+ return cur.getId();
+ }
+ @Override
+ public DateTime getEffectiveDate() {
+ return cur.getEffectiveDate();
+ }
+ });
+ prevProductName = productName;
+ prevBillingPeriod = billingPeriod;
+ prevPriceListName = priceListName;
+ prevPhaseType = phaseType;
+ }
+ }
+ */
+
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<DeletedEvent> getDeletedEvents() {
+ return deletedEvents;
+ }
+
+ @Override
+ public List<NewEvent> getNewEvents() {
+ return newEvents;
+ }
+
+ @Override
+ public List<ExistingEvent> getExistingEvents() {
+ return existingEvents;
+ }
+
+ @Override
+ public long getActiveVersion() {
+ return activeVersion;
+ }
+
+
+ private void sortExistingEvent(final List<ExistingEvent> events) {
+ if (events != null) {
+ Collections.sort(events, new Comparator<ExistingEvent>() {
+ @Override
+ public int compare(final ExistingEvent arg0, final ExistingEvent arg1) {
+ return arg0.getEffectiveDate().compareTo(arg1.getEffectiveDate());
+ }
+ });
+ }
+ }
+
+ private void sortNewEvent(final List<NewEvent> events) {
+ if (events != null) {
+ Collections.sort(events, new Comparator<NewEvent>() {
+ @Override
+ public int compare(final NewEvent arg0, final NewEvent arg1) {
+ return arg0.getRequestedDate().compareTo(arg1.getRequestedDate());
+ }
+ });
+ }
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java
new file mode 100644
index 0000000..44119f5
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/DefaultSubscriptionBaseTimelineApi.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.subscription.api.SubscriptionApiBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
+import org.killbill.billing.subscription.engine.addon.AddonUtils;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.glue.DefaultSubscriptionModule;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.NewEvent;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.clock.Clock;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+public class DefaultSubscriptionBaseTimelineApi extends SubscriptionApiBase implements SubscriptionBaseTimelineApi {
+
+ private final RepairSubscriptionLifecycleDao repairDao;
+ private final CatalogService catalogService;
+ private final InternalCallContextFactory internalCallContextFactory;
+ private final AddonUtils addonUtils;
+
+ private final SubscriptionBaseApiService repairApiService;
+
+ private enum RepairType {
+ BASE_REPAIR,
+ ADD_ON_REPAIR,
+ STANDALONE_REPAIR
+ }
+
+ @Inject
+ public DefaultSubscriptionBaseTimelineApi(final CatalogService catalogService,
+ final SubscriptionBaseApiService apiService,
+ @Named(DefaultSubscriptionModule.REPAIR_NAMED) final RepairSubscriptionLifecycleDao repairDao, final SubscriptionDao dao,
+ @Named(DefaultSubscriptionModule.REPAIR_NAMED) final SubscriptionBaseApiService repairApiService,
+ final InternalCallContextFactory internalCallContextFactory, final Clock clock, final AddonUtils addonUtils) {
+ super(dao, apiService, clock, catalogService);
+ this.catalogService = catalogService;
+ this.repairDao = repairDao;
+ this.internalCallContextFactory = internalCallContextFactory;
+ this.repairApiService = repairApiService;
+ this.addonUtils = addonUtils;
+ }
+
+ @Override
+ public BundleBaseTimeline getBundleTimeline(final SubscriptionBaseBundle bundle, final TenantContext context)
+ throws SubscriptionBaseRepairException {
+ return getBundleTimelineInternal(bundle, bundle.getExternalKey(), context);
+ }
+
+ @Override
+ public BundleBaseTimeline getBundleTimeline(final UUID accountId, final String bundleName, final TenantContext context)
+ throws SubscriptionBaseRepairException {
+ final List<SubscriptionBaseBundle> bundles = dao.getSubscriptionBundlesForAccountAndKey(accountId, bundleName, internalCallContextFactory.createInternalTenantContext(context));
+ final SubscriptionBaseBundle bundle = bundles.size() > 0 ? bundles.get(bundles.size() - 1) : null;
+ return getBundleTimelineInternal(bundle, bundleName + " [accountId= " + accountId.toString() + "]", context);
+ }
+
+ @Override
+ public BundleBaseTimeline getBundleTimeline(final UUID bundleId, final TenantContext context) throws SubscriptionBaseRepairException {
+
+ final SubscriptionBaseBundle bundle = dao.getSubscriptionBundleFromId(bundleId, internalCallContextFactory.createInternalTenantContext(context));
+ return getBundleTimelineInternal(bundle, bundleId.toString(), context);
+ }
+
+ private BundleBaseTimeline getBundleTimelineInternal(final SubscriptionBaseBundle bundle, final String descBundle, final TenantContext context) throws SubscriptionBaseRepairException {
+ try {
+ if (bundle == null) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_BUNDLE, descBundle);
+ }
+ final List<SubscriptionDataRepair> subscriptions = convertToSubscriptionsDataRepair(dao.getSubscriptions(bundle.getId(), internalCallContextFactory.createInternalTenantContext(context)));
+ if (subscriptions.size() == 0) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NO_ACTIVE_SUBSCRIPTIONS, bundle.getId());
+ }
+ final String viewId = getViewId(((DefaultSubscriptionBaseBundle) bundle).getLastSysUpdateDate(), subscriptions);
+ final List<SubscriptionBaseTimeline> repairs = createGetSubscriptionRepairList(subscriptions, Collections.<SubscriptionBaseTimeline>emptyList());
+ return createGetBundleRepair(bundle.getId(), bundle.getExternalKey(), viewId, repairs);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseRepairException(e);
+ }
+ }
+
+ private List<SubscriptionDataRepair> convertToSubscriptionsDataRepair(List<SubscriptionBase> input) {
+ return new ArrayList<SubscriptionDataRepair>(Collections2.transform(input, new Function<SubscriptionBase, SubscriptionDataRepair>() {
+ @Override
+ public SubscriptionDataRepair apply(@Nullable final SubscriptionBase subscription) {
+ return convertToSubscriptionDataRepair((DefaultSubscriptionBase) subscription);
+ }
+ }));
+ }
+ private SubscriptionDataRepair convertToSubscriptionDataRepair(DefaultSubscriptionBase input) {
+ return new SubscriptionDataRepair(input, repairApiService, (SubscriptionDao) repairDao, clock, addonUtils, catalogService, internalCallContextFactory);
+ }
+
+ @Override
+ public BundleBaseTimeline repairBundle(final BundleBaseTimeline input, final boolean dryRun, final CallContext context) throws SubscriptionBaseRepairException {
+ final InternalTenantContext tenantContext = internalCallContextFactory.createInternalTenantContext(context);
+ try {
+ final SubscriptionBaseBundle bundle = dao.getSubscriptionBundleFromId(input.getId(), tenantContext);
+ if (bundle == null) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_BUNDLE, input.getId());
+ }
+
+ // Subscriptions are ordered with BASE subscription first-- if exists
+ final List<SubscriptionDataRepair> subscriptions = convertToSubscriptionsDataRepair(dao.getSubscriptions(input.getId(), tenantContext));
+ if (subscriptions.size() == 0) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NO_ACTIVE_SUBSCRIPTIONS, input.getId());
+ }
+
+ final String viewId = getViewId(((DefaultSubscriptionBaseBundle) bundle).getLastSysUpdateDate(), subscriptions);
+ if (!viewId.equals(input.getViewId())) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_VIEW_CHANGED, input.getId(), input.getViewId(), viewId);
+ }
+
+ DateTime firstDeletedBPEventTime = null;
+ DateTime lastRemainingBPEventTime = null;
+
+ boolean isBasePlanRecreate = false;
+ DateTime newBundleStartDate = null;
+
+ SubscriptionDataRepair baseSubscriptionRepair = null;
+ final List<SubscriptionDataRepair> addOnSubscriptionInRepair = new LinkedList<SubscriptionDataRepair>();
+ final List<SubscriptionDataRepair> inRepair = new LinkedList<SubscriptionDataRepair>();
+ for (final SubscriptionBase cur : subscriptions) {
+ final SubscriptionBaseTimeline curRepair = findAndCreateSubscriptionRepair(cur.getId(), input.getSubscriptions());
+ if (curRepair != null) {
+ final SubscriptionDataRepair curInputRepair = ((SubscriptionDataRepair) cur);
+ final List<SubscriptionBaseEvent> remaining = getRemainingEventsAndValidateDeletedEvents(curInputRepair, firstDeletedBPEventTime, curRepair.getDeletedEvents());
+
+ final boolean isPlanRecreate = (curRepair.getNewEvents().size() > 0
+ && (curRepair.getNewEvents().get(0).getSubscriptionTransitionType() == SubscriptionBaseTransitionType.CREATE
+ || curRepair.getNewEvents().get(0).getSubscriptionTransitionType() == SubscriptionBaseTransitionType.RE_CREATE));
+
+ final DateTime newSubscriptionStartDate = isPlanRecreate ? curRepair.getNewEvents().get(0).getRequestedDate() : null;
+
+ if (isPlanRecreate && remaining.size() != 0) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_SUB_RECREATE_NOT_EMPTY, cur.getId(), cur.getBundleId());
+ }
+
+ if (!isPlanRecreate && remaining.size() == 0) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_SUB_EMPTY, cur.getId(), cur.getBundleId());
+ }
+
+ if (cur.getCategory() == ProductCategory.BASE) {
+
+ final int bpTransitionSize = ((DefaultSubscriptionBase) cur).getAllTransitions().size();
+ lastRemainingBPEventTime = (remaining.size() > 0) ? curInputRepair.getAllTransitions().get(remaining.size() - 1).getEffectiveTransitionTime() : null;
+ firstDeletedBPEventTime = (remaining.size() < bpTransitionSize) ? curInputRepair.getAllTransitions().get(remaining.size()).getEffectiveTransitionTime() : null;
+
+ isBasePlanRecreate = isPlanRecreate;
+ newBundleStartDate = newSubscriptionStartDate;
+ }
+
+ if (curRepair.getNewEvents().size() > 0) {
+ final DateTime lastRemainingEventTime = (remaining.size() == 0) ? null : curInputRepair.getAllTransitions().get(remaining.size() - 1).getEffectiveTransitionTime();
+ validateFirstNewEvent(curInputRepair, curRepair.getNewEvents().get(0), lastRemainingBPEventTime, lastRemainingEventTime);
+ }
+
+ final SubscriptionDataRepair curOutputRepair = createSubscriptionDataRepair(curInputRepair, newBundleStartDate, newSubscriptionStartDate, remaining);
+ repairDao.initializeRepair(curInputRepair.getId(), remaining, tenantContext);
+ inRepair.add(curOutputRepair);
+ if (curOutputRepair.getCategory() == ProductCategory.ADD_ON) {
+ // Check if ADD_ON RE_CREATE is before BP start
+ if (isPlanRecreate && (subscriptions.get(0)).getStartDate().isAfter(curRepair.getNewEvents().get(0).getRequestedDate())) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_AO_CREATE_BEFORE_BP_START, cur.getId(), cur.getBundleId());
+ }
+ addOnSubscriptionInRepair.add(curOutputRepair);
+ } else if (curOutputRepair.getCategory() == ProductCategory.BASE) {
+ baseSubscriptionRepair = curOutputRepair;
+ }
+ }
+ }
+
+ final RepairType repairType = getRepairType(subscriptions.get(0), (baseSubscriptionRepair != null));
+ switch (repairType) {
+ case BASE_REPAIR:
+ // We need to add any existing addon that are not in the input repair list
+ for (final SubscriptionBase cur : subscriptions) {
+ if (cur.getCategory() == ProductCategory.ADD_ON && !inRepair.contains(cur)) {
+ final SubscriptionDataRepair curOutputRepair = createSubscriptionDataRepair((SubscriptionDataRepair) cur, newBundleStartDate, null, ((SubscriptionDataRepair) cur).getEvents());
+ repairDao.initializeRepair(curOutputRepair.getId(), ((SubscriptionDataRepair) cur).getEvents(), tenantContext);
+ inRepair.add(curOutputRepair);
+ addOnSubscriptionInRepair.add(curOutputRepair);
+ }
+ }
+ break;
+ case ADD_ON_REPAIR:
+ // We need to set the baseSubscription as it is useful to calculate addon validity
+ final SubscriptionDataRepair baseSubscription = (SubscriptionDataRepair) subscriptions.get(0);
+ baseSubscriptionRepair = createSubscriptionDataRepair(baseSubscription, baseSubscription.getBundleStartDate(), baseSubscription.getAlignStartDate(), baseSubscription.getEvents());
+ break;
+ case STANDALONE_REPAIR:
+ default:
+ break;
+ }
+
+ validateBasePlanRecreate(isBasePlanRecreate, subscriptions, input.getSubscriptions());
+ validateInputSubscriptionsKnown(subscriptions, input.getSubscriptions());
+
+ final Collection<NewEvent> newEvents = createOrderedNewEventInput(input.getSubscriptions());
+ for (final NewEvent newEvent : newEvents) {
+ final DefaultNewEvent cur = (DefaultNewEvent) newEvent;
+ final SubscriptionDataRepair curDataRepair = findSubscriptionDataRepair(cur.getSubscriptionId(), inRepair);
+ if (curDataRepair == null) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_SUBSCRIPTION, cur.getSubscriptionId());
+ }
+ curDataRepair.addNewRepairEvent(cur, baseSubscriptionRepair, addOnSubscriptionInRepair, context);
+ }
+
+ if (dryRun) {
+ baseSubscriptionRepair.addFutureAddonCancellation(addOnSubscriptionInRepair, context);
+
+ final List<SubscriptionBaseTimeline> repairs = createGetSubscriptionRepairList(subscriptions, convertDataRepair(inRepair));
+ return createGetBundleRepair(input.getId(), bundle.getExternalKey(), input.getViewId(), repairs);
+ } else {
+ dao.repair(bundle.getAccountId(), input.getId(), inRepair, internalCallContextFactory.createInternalCallContext(bundle.getAccountId(), context));
+ return getBundleTimeline(input.getId(), context);
+ }
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseRepairException(e);
+ } finally {
+ repairDao.cleanup(tenantContext);
+ }
+ }
+
+ private RepairType getRepairType(final SubscriptionBase firstSubscription, final boolean gotBaseSubscription) {
+ if (firstSubscription.getCategory() == ProductCategory.BASE) {
+ return gotBaseSubscription ? RepairType.BASE_REPAIR : RepairType.ADD_ON_REPAIR;
+ } else {
+ return RepairType.STANDALONE_REPAIR;
+ }
+ }
+
+ private void validateBasePlanRecreate(final boolean isBasePlanRecreate, final List<SubscriptionDataRepair> subscriptions, final List<SubscriptionBaseTimeline> input)
+ throws SubscriptionBaseRepairException {
+ if (!isBasePlanRecreate) {
+ return;
+ }
+ if (subscriptions.size() != input.size()) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO, subscriptions.get(0).getBundleId());
+ }
+ for (final SubscriptionBaseTimeline cur : input) {
+ if (cur.getNewEvents().size() != 0
+ && (cur.getNewEvents().get(0).getSubscriptionTransitionType() != SubscriptionBaseTransitionType.CREATE
+ && cur.getNewEvents().get(0).getSubscriptionTransitionType() != SubscriptionBaseTransitionType.RE_CREATE)) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO_CREATE, subscriptions.get(0).getBundleId());
+ }
+ }
+ }
+
+ private void validateInputSubscriptionsKnown(final List<SubscriptionDataRepair> subscriptions, final List<SubscriptionBaseTimeline> input)
+ throws SubscriptionBaseRepairException {
+ for (final SubscriptionBaseTimeline cur : input) {
+ boolean found = false;
+ for (final SubscriptionBase s : subscriptions) {
+ if (s.getId().equals(cur.getId())) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_SUBSCRIPTION, cur.getId());
+ }
+ }
+ }
+
+ private void validateFirstNewEvent(final DefaultSubscriptionBase data, final NewEvent firstNewEvent, final DateTime lastBPRemainingTime, final DateTime lastRemainingTime)
+ throws SubscriptionBaseRepairException {
+ if (lastBPRemainingTime != null &&
+ firstNewEvent.getRequestedDate().isBefore(lastBPRemainingTime)) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_BP_REMAINING, firstNewEvent.getSubscriptionTransitionType(), data.getId());
+ }
+ if (lastRemainingTime != null &&
+ firstNewEvent.getRequestedDate().isBefore(lastRemainingTime)) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_AO_REMAINING, firstNewEvent.getSubscriptionTransitionType(), data.getId());
+ }
+
+ }
+
+ private Collection<NewEvent> createOrderedNewEventInput(final List<SubscriptionBaseTimeline> subscriptionsReapir) {
+ final TreeSet<NewEvent> newEventSet = new TreeSet<SubscriptionBaseTimeline.NewEvent>(new Comparator<NewEvent>() {
+ @Override
+ public int compare(final NewEvent o1, final NewEvent o2) {
+ return o1.getRequestedDate().compareTo(o2.getRequestedDate());
+ }
+ });
+ for (final SubscriptionBaseTimeline cur : subscriptionsReapir) {
+ for (final NewEvent e : cur.getNewEvents()) {
+ newEventSet.add(new DefaultNewEvent(cur.getId(), e.getPlanPhaseSpecifier(), e.getRequestedDate(), e.getSubscriptionTransitionType()));
+ }
+ }
+
+ return newEventSet;
+ }
+
+ private List<SubscriptionBaseEvent> getRemainingEventsAndValidateDeletedEvents(final SubscriptionDataRepair data, final DateTime firstBPDeletedTime,
+ final List<SubscriptionBaseTimeline.DeletedEvent> deletedEvents)
+ throws SubscriptionBaseRepairException {
+ if (deletedEvents == null || deletedEvents.size() == 0) {
+ return data.getEvents();
+ }
+
+ int nbDeleted = 0;
+ final LinkedList<SubscriptionBaseEvent> result = new LinkedList<SubscriptionBaseEvent>();
+ for (final SubscriptionBaseEvent cur : data.getEvents()) {
+
+ boolean foundDeletedEvent = false;
+ for (final SubscriptionBaseTimeline.DeletedEvent d : deletedEvents) {
+ if (cur.getId().equals(d.getEventId())) {
+ foundDeletedEvent = true;
+ nbDeleted++;
+ break;
+ }
+ }
+ if (!foundDeletedEvent && nbDeleted > 0) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_INVALID_DELETE_SET, cur.getId(), data.getId());
+ }
+ if (firstBPDeletedTime != null &&
+ !cur.getEffectiveDate().isBefore(firstBPDeletedTime) &&
+ !foundDeletedEvent) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_MISSING_AO_DELETE_EVENT, cur.getId(), data.getId());
+ }
+
+ if (nbDeleted == 0) {
+ result.add(cur);
+ }
+ }
+
+ if (nbDeleted != deletedEvents.size()) {
+ for (final SubscriptionBaseTimeline.DeletedEvent d : deletedEvents) {
+ boolean found = false;
+ for (final SubscriptionBaseTransition cur : data.getAllTransitions()) {
+ if (((SubscriptionBaseTransitionData) cur).getId().equals(d.getEventId())) {
+ found = true;
+ }
+ }
+ if (!found) {
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_NON_EXISTENT_DELETE_EVENT, d.getEventId(), data.getId());
+ }
+ }
+
+ }
+
+ return result;
+ }
+
+ private String getViewId(final DateTime lastUpdateBundleDate, final List<SubscriptionDataRepair> subscriptions) {
+ final StringBuilder tmp = new StringBuilder();
+ long lastOrderedId = -1;
+ for (final SubscriptionBase cur : subscriptions) {
+ lastOrderedId = lastOrderedId < ((DefaultSubscriptionBase) cur).getLastEventOrderedId() ? ((DefaultSubscriptionBase) cur).getLastEventOrderedId() : lastOrderedId;
+ }
+ tmp.append(lastOrderedId);
+ tmp.append("-");
+ tmp.append(lastUpdateBundleDate.toDate().getTime());
+
+ return tmp.toString();
+ }
+
+ private BundleBaseTimeline createGetBundleRepair(final UUID bundleId, final String externalKey, final String viewId, final List<SubscriptionBaseTimeline> repairList) {
+ return new BundleBaseTimeline() {
+ @Override
+ public String getViewId() {
+ return viewId;
+ }
+
+ @Override
+ public List<SubscriptionBaseTimeline> getSubscriptions() {
+ return repairList;
+ }
+
+ @Override
+ public UUID getId() {
+ return bundleId;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getExternalKey() {
+ return externalKey;
+ }
+ };
+ }
+
+ private List<SubscriptionBaseTimeline> createGetSubscriptionRepairList(final List<SubscriptionDataRepair> subscriptions, final List<SubscriptionBaseTimeline> inRepair) throws CatalogApiException {
+
+ final List<SubscriptionBaseTimeline> result = new LinkedList<SubscriptionBaseTimeline>();
+ final Set<UUID> repairIds = new TreeSet<UUID>();
+ for (final SubscriptionBaseTimeline cur : inRepair) {
+ repairIds.add(cur.getId());
+ result.add(cur);
+ }
+
+ for (final SubscriptionBase cur : subscriptions) {
+ if (!repairIds.contains(cur.getId())) {
+ result.add(new DefaultSubscriptionBaseTimeline((SubscriptionDataRepair) cur, catalogService.getFullCatalog()));
+ }
+ }
+
+ return result;
+ }
+
+ private List<SubscriptionBaseTimeline> convertDataRepair(final List<SubscriptionDataRepair> input) throws CatalogApiException {
+ final List<SubscriptionBaseTimeline> result = new LinkedList<SubscriptionBaseTimeline>();
+ for (final SubscriptionDataRepair cur : input) {
+ result.add(new DefaultSubscriptionBaseTimeline(cur, catalogService.getFullCatalog()));
+ }
+
+ return result;
+ }
+
+ private SubscriptionDataRepair findSubscriptionDataRepair(final UUID targetId, final List<SubscriptionDataRepair> input) {
+ for (final SubscriptionDataRepair cur : input) {
+ if (cur.getId().equals(targetId)) {
+ return cur;
+ }
+ }
+
+ return null;
+ }
+
+ private SubscriptionDataRepair createSubscriptionDataRepair(final DefaultSubscriptionBase curData, final DateTime newBundleStartDate, final DateTime newSubscriptionStartDate, final List<SubscriptionBaseEvent> initialEvents) {
+ final SubscriptionBuilder builder = new SubscriptionBuilder(curData);
+ builder.setActiveVersion(curData.getActiveVersion() + 1);
+ if (newBundleStartDate != null) {
+ builder.setBundleStartDate(newBundleStartDate);
+ }
+ if (newSubscriptionStartDate != null) {
+ builder.setAlignStartDate(newSubscriptionStartDate);
+ }
+ if (initialEvents.size() > 0) {
+ for (final SubscriptionBaseEvent cur : initialEvents) {
+ cur.setActiveVersion(builder.getActiveVersion());
+ }
+ }
+
+ final SubscriptionDataRepair subscriptiondataRepair = new SubscriptionDataRepair(builder, curData.getEvents(), repairApiService, (SubscriptionDao) repairDao, clock, addonUtils, catalogService, internalCallContextFactory);
+ subscriptiondataRepair.rebuildTransitions(curData.getEvents(), catalogService.getFullCatalog());
+ return subscriptiondataRepair;
+ }
+
+ private SubscriptionBaseTimeline findAndCreateSubscriptionRepair(final UUID target, final List<SubscriptionBaseTimeline> input) {
+ for (final SubscriptionBaseTimeline cur : input) {
+ if (target.equals(cur.getId())) {
+ return new DefaultSubscriptionBaseTimeline(cur);
+ }
+ }
+
+ return null;
+ }
+}
+
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java
new file mode 100644
index 0000000..2d1da3d
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionApiService.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.clock.Clock;
+import org.killbill.billing.subscription.alignment.PlanAligner;
+import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseApiService;
+import org.killbill.billing.subscription.engine.addon.AddonUtils;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.glue.DefaultSubscriptionModule;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+public class RepairSubscriptionApiService extends DefaultSubscriptionBaseApiService implements SubscriptionBaseApiService {
+
+ @Inject
+ public RepairSubscriptionApiService(final Clock clock,
+ @Named(DefaultSubscriptionModule.REPAIR_NAMED) final SubscriptionDao dao,
+ final CatalogService catalogService,
+ final PlanAligner planAligner,
+ final AddonUtils addonUtils,
+ final InternalCallContextFactory internalCallContextFactory) {
+ super(clock, dao, catalogService, planAligner, addonUtils, internalCallContextFactory);
+ }
+
+ // Nothing to do for repair as we pass all the repair events in the stream
+ @Override
+ public int cancelAddOnsIfRequired(final DefaultSubscriptionBase baseSubscription, final DateTime effectiveDate, final InternalCallContext context) {
+ return 0;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionLifecycleDao.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionLifecycleDao.java
new file mode 100644
index 0000000..dac507f
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/RepairSubscriptionLifecycleDao.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+public interface RepairSubscriptionLifecycleDao {
+
+ public void initializeRepair(UUID subscriptionId, List<SubscriptionBaseEvent> initialEvents, InternalTenantContext context);
+
+ public void cleanup(InternalTenantContext context);
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionDataRepair.java b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionDataRepair.java
new file mode 100644
index 0000000..d06c888
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/timeline/SubscriptionDataRepair.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.engine.addon.AddonUtils;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.user.ApiEventBuilder;
+import org.killbill.billing.subscription.events.user.ApiEventCancel;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.clock.Clock;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+
+public class SubscriptionDataRepair extends DefaultSubscriptionBase {
+
+ private final AddonUtils addonUtils;
+ private final Clock clock;
+ private final SubscriptionDao repairDao;
+ private final CatalogService catalogService;
+ private final List<SubscriptionBaseEvent> initialEvents;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+
+ public SubscriptionDataRepair(final SubscriptionBuilder builder, final List<SubscriptionBaseEvent> initialEvents, final SubscriptionBaseApiService apiService,
+ final SubscriptionDao dao, final Clock clock, final AddonUtils addonUtils, final CatalogService catalogService,
+ final InternalCallContextFactory internalCallContextFactory) {
+ super(builder, apiService, clock);
+ this.repairDao = dao;
+ this.addonUtils = addonUtils;
+ this.clock = clock;
+ this.catalogService = catalogService;
+ this.initialEvents = initialEvents;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+
+
+ public SubscriptionDataRepair(final DefaultSubscriptionBase defaultSubscriptionBase, final SubscriptionBaseApiService apiService,
+ final SubscriptionDao dao, final Clock clock, final AddonUtils addonUtils, final CatalogService catalogService,
+ final InternalCallContextFactory internalCallContextFactory) {
+ super(defaultSubscriptionBase, apiService , clock);
+ this.repairDao = dao;
+ this.addonUtils = addonUtils;
+ this.clock = clock;
+ this.catalogService = catalogService;
+ this.initialEvents = defaultSubscriptionBase.getEvents();
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ DateTime getLastUserEventEffectiveDate() {
+ DateTime res = null;
+ for (final SubscriptionBaseEvent cur : events) {
+ if (cur.getActiveVersion() != getActiveVersion()) {
+ break;
+ }
+ if (cur.getType() == EventType.PHASE) {
+ continue;
+ }
+ res = cur.getEffectiveDate();
+ }
+ return res;
+ }
+
+ public void addNewRepairEvent(final DefaultNewEvent input, final SubscriptionDataRepair baseSubscription, final List<SubscriptionDataRepair> addonSubscriptions, final CallContext context)
+ throws SubscriptionBaseRepairException {
+
+ try {
+ final PlanPhaseSpecifier spec = input.getPlanPhaseSpecifier();
+ switch (input.getSubscriptionTransitionType()) {
+ case CREATE:
+ case RE_CREATE:
+ recreate(spec, input.getRequestedDate(), context);
+ checkAddonRights(baseSubscription);
+ break;
+ case CHANGE:
+ changePlanWithDate(spec.getProductName(), spec.getBillingPeriod(), spec.getPriceListName(), input.getRequestedDate(), context);
+ checkAddonRights(baseSubscription);
+ trickleDownBPEffectForAddon(addonSubscriptions, getLastUserEventEffectiveDate(), context);
+ break;
+ case CANCEL:
+ cancelWithDate(input.getRequestedDate(), context);
+ trickleDownBPEffectForAddon(addonSubscriptions, getLastUserEventEffectiveDate(), context);
+ break;
+ case PHASE:
+ break;
+ default:
+ throw new SubscriptionBaseRepairException(ErrorCode.SUB_REPAIR_UNKNOWN_TYPE, input.getSubscriptionTransitionType(), id);
+ }
+ } catch (SubscriptionBaseApiException e) {
+ throw new SubscriptionBaseRepairException(e);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseRepairException(e);
+ }
+ }
+
+ public void addFutureAddonCancellation(final List<SubscriptionDataRepair> addOnSubscriptionInRepair, final CallContext context) {
+
+ if (getCategory() != ProductCategory.BASE) {
+ return;
+ }
+
+ final SubscriptionBaseTransition pendingTransition = getPendingTransition();
+ if (pendingTransition == null) {
+ return;
+ }
+ final Product baseProduct = (pendingTransition.getTransitionType() == SubscriptionBaseTransitionType.CANCEL) ? null :
+ pendingTransition.getNextPlan().getProduct();
+
+ addAddonCancellationIfRequired(addOnSubscriptionInRepair, baseProduct, pendingTransition.getEffectiveTransitionTime(), context);
+ }
+
+ private void trickleDownBPEffectForAddon(final List<SubscriptionDataRepair> addOnSubscriptionInRepair, final DateTime effectiveDate, final CallContext context)
+ throws SubscriptionBaseApiException {
+
+ if (getCategory() != ProductCategory.BASE) {
+ return;
+ }
+
+ final Product baseProduct = (getState() == EntitlementState.CANCELLED) ?
+ null : getCurrentPlan().getProduct();
+ addAddonCancellationIfRequired(addOnSubscriptionInRepair, baseProduct, effectiveDate, context);
+ }
+
+ private void addAddonCancellationIfRequired(final List<SubscriptionDataRepair> addOnSubscriptionInRepair, final Product baseProduct,
+ final DateTime effectiveDate, final CallContext context) {
+
+ final DateTime now = clock.getUTCNow();
+ final Iterator<SubscriptionDataRepair> it = addOnSubscriptionInRepair.iterator();
+ while (it.hasNext()) {
+ final SubscriptionDataRepair cur = it.next();
+ if (cur.getState() == EntitlementState.CANCELLED ||
+ cur.getCategory() != ProductCategory.ADD_ON) {
+ continue;
+ }
+ final Plan addonCurrentPlan = cur.getCurrentPlan();
+ if (baseProduct == null ||
+ addonUtils.isAddonIncluded(baseProduct, addonCurrentPlan) ||
+ !addonUtils.isAddonAvailable(baseProduct, addonCurrentPlan)) {
+
+ final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
+ .setSubscriptionId(cur.getId())
+ .setActiveVersion(cur.getActiveVersion())
+ .setProcessedDate(now)
+ .setEffectiveDate(effectiveDate)
+ .setRequestedDate(now)
+ .setFromDisk(true));
+ repairDao.cancelSubscription(cur, cancelEvent, internalCallContextFactory.createInternalCallContext(cur.getId(), ObjectType.SUBSCRIPTION, context), 0);
+ cur.rebuildTransitions(repairDao.getEventsForSubscription(cur.getId(), internalCallContextFactory.createInternalTenantContext(context)), catalogService.getFullCatalog());
+ }
+ }
+ }
+
+ private void checkAddonRights(final SubscriptionDataRepair baseSubscription)
+ throws SubscriptionBaseApiException, CatalogApiException {
+ if (getCategory() == ProductCategory.ADD_ON) {
+ addonUtils.checkAddonCreationRights(baseSubscription, getCurrentPlan());
+ }
+ }
+
+ public List<SubscriptionBaseEvent> getEvents() {
+ return events;
+ }
+
+ public List<SubscriptionBaseEvent> getInitialEvents() {
+ return initialEvents;
+ }
+
+ public Collection<SubscriptionBaseEvent> getNewEvents() {
+ return Collections2.filter(events, new Predicate<SubscriptionBaseEvent>() {
+ @Override
+ public boolean apply(final SubscriptionBaseEvent input) {
+ return !initialEvents.contains(input);
+ }
+ });
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
new file mode 100644
index 0000000..eb47278
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.transfer;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionApiBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData.BundleMigrationData;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData.SubscriptionMigrationData;
+import org.killbill.billing.subscription.api.svcs.DefaultSubscriptionInternalApi;
+import org.killbill.billing.subscription.api.timeline.BundleBaseTimeline;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseRepairException;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.ExistingEvent;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineApi;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.phase.PhaseEventData;
+import org.killbill.billing.subscription.events.user.ApiEventBuilder;
+import org.killbill.billing.subscription.events.user.ApiEventCancel;
+import org.killbill.billing.subscription.events.user.ApiEventChange;
+import org.killbill.billing.subscription.events.user.ApiEventTransfer;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+public class DefaultSubscriptionBaseTransferApi extends SubscriptionApiBase implements SubscriptionBaseTransferApi {
+
+ private final CatalogService catalogService;
+ private final SubscriptionBaseTimelineApi timelineApi;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public DefaultSubscriptionBaseTransferApi(final Clock clock, final SubscriptionDao dao, final SubscriptionBaseTimelineApi timelineApi, final CatalogService catalogService,
+ final SubscriptionBaseApiService apiService, final InternalCallContextFactory internalCallContextFactory) {
+ super(dao, apiService, clock, catalogService);
+ this.catalogService = catalogService;
+ this.timelineApi = timelineApi;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ private SubscriptionBaseEvent createEvent(final boolean firstEvent, final ExistingEvent existingEvent, final DefaultSubscriptionBase subscription, final DateTime transferDate, final CallContext context)
+ throws CatalogApiException {
+
+ SubscriptionBaseEvent newEvent = null;
+
+ final Catalog catalog = catalogService.getFullCatalog();
+
+ final DateTime effectiveDate = existingEvent.getEffectiveDate().isBefore(transferDate) ? transferDate : existingEvent.getEffectiveDate();
+
+ final PlanPhaseSpecifier spec = existingEvent.getPlanPhaseSpecifier();
+ final PlanPhase currentPhase = existingEvent.getPlanPhaseName() != null ? catalog.findPhase(existingEvent.getPlanPhaseName(), effectiveDate, subscription.getAlignStartDate()) : null;
+
+ if (spec == null || currentPhase == null) {
+ // Ignore cancellations - we assume that transferred subscriptions should always be active
+ return null;
+ }
+ final ApiEventBuilder apiBuilder = new ApiEventBuilder()
+ .setSubscriptionId(subscription.getId())
+ .setEventPlan(currentPhase.getPlan().getName())
+ .setEventPlanPhase(currentPhase.getName())
+ .setEventPriceList(spec.getPriceListName())
+ .setActiveVersion(subscription.getActiveVersion())
+ .setProcessedDate(clock.getUTCNow())
+ .setEffectiveDate(effectiveDate)
+ .setRequestedDate(effectiveDate)
+ .setFromDisk(true);
+
+ switch (existingEvent.getSubscriptionTransitionType()) {
+ case TRANSFER:
+ case MIGRATE_ENTITLEMENT:
+ case RE_CREATE:
+ case CREATE:
+ newEvent = new ApiEventTransfer(apiBuilder);
+ break;
+
+ // Should we even keep future change events; product question really
+ case CHANGE:
+ newEvent = firstEvent ? new ApiEventTransfer(apiBuilder) : new ApiEventChange(apiBuilder);
+ break;
+
+ case PHASE:
+ newEvent = firstEvent ? new ApiEventTransfer(apiBuilder) :
+ PhaseEventData.createNextPhaseEvent(currentPhase.getName(), subscription, clock.getUTCNow(), effectiveDate);
+ break;
+
+ // Ignore these events except if it's the first event for the new subscription
+ case MIGRATE_BILLING:
+ if (firstEvent) {
+ newEvent = new ApiEventTransfer(apiBuilder);
+ }
+ break;
+ case CANCEL:
+ break;
+
+ default:
+ throw new SubscriptionBaseError(String.format("Unexpected transitionType %s", existingEvent.getSubscriptionTransitionType()));
+ }
+ return newEvent;
+ }
+
+ @VisibleForTesting
+ List<SubscriptionBaseEvent> toEvents(final List<ExistingEvent> existingEvents, final DefaultSubscriptionBase subscription,
+ final DateTime transferDate, final CallContext context) throws SubscriptionBaseTransferApiException {
+
+ try {
+ final List<SubscriptionBaseEvent> result = new LinkedList<SubscriptionBaseEvent>();
+
+ SubscriptionBaseEvent event = null;
+ ExistingEvent prevEvent = null;
+ boolean firstEvent = true;
+ for (ExistingEvent cur : existingEvents) {
+ // Skip all events prior to the transferDate
+ if (cur.getEffectiveDate().isBefore(transferDate)) {
+ prevEvent = cur;
+ continue;
+ }
+
+ // Add previous event the first time if needed
+ if (prevEvent != null) {
+ event = createEvent(firstEvent, prevEvent, subscription, transferDate, context);
+ if (event != null) {
+ result.add(event);
+ firstEvent = false;
+ }
+ prevEvent = null;
+ }
+
+ event = createEvent(firstEvent, cur, subscription, transferDate, context);
+ if (event != null) {
+ result.add(event);
+ firstEvent = false;
+ }
+ }
+
+ // Previous loop did not get anything because transferDate is greater than effectiveDate of last event
+ if (prevEvent != null) {
+ event = createEvent(firstEvent, prevEvent, subscription, transferDate, context);
+ if (event != null) {
+ result.add(event);
+ }
+ prevEvent = null;
+ }
+
+ return result;
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseTransferApiException(e);
+ }
+ }
+
+ @Override
+ public SubscriptionBaseBundle transferBundle(final UUID sourceAccountId, final UUID destAccountId,
+ final String bundleKey, final DateTime transferDate, final boolean transferAddOn,
+ final boolean cancelImmediately, final CallContext context) throws SubscriptionBaseTransferApiException {
+ final InternalCallContext fromInternalCallContext = internalCallContextFactory.createInternalCallContext(sourceAccountId, context);
+ final InternalCallContext toInternalCallContext = internalCallContextFactory.createInternalCallContext(destAccountId, context);
+
+ try {
+ final DateTime effectiveTransferDate = transferDate == null ? clock.getUTCNow() : transferDate;
+ if (effectiveTransferDate.isAfter(clock.getUTCNow())) {
+ // The transfer event for the migrated bundle will be the first one, which cannot be in the future
+ // (subscription always expects the first event to be in the past)
+ throw new SubscriptionBaseTransferApiException(ErrorCode.SUB_TRANSFER_INVALID_EFF_DATE, effectiveTransferDate);
+ }
+
+ final List<SubscriptionBaseBundle> bundlesForAccountAndKey = dao.getSubscriptionBundlesForAccountAndKey(sourceAccountId, bundleKey, fromInternalCallContext);
+ final SubscriptionBaseBundle bundle = DefaultSubscriptionInternalApi.getActiveBundleForKeyNotException(bundlesForAccountAndKey, dao, clock, fromInternalCallContext);
+ if (bundle == null) {
+ throw new SubscriptionBaseTransferApiException(ErrorCode.SUB_CREATE_NO_BUNDLE, bundleKey);
+ }
+
+ // Get the bundle timeline for the old account
+ final BundleBaseTimeline bundleBaseTimeline = timelineApi.getBundleTimeline(bundle, context);
+
+ final DefaultSubscriptionBaseBundle subscriptionBundleData = new DefaultSubscriptionBaseBundle(bundleKey, destAccountId, effectiveTransferDate,
+ bundle.getOriginalCreatedDate(), clock.getUTCNow(), clock.getUTCNow());
+ final List<SubscriptionMigrationData> subscriptionMigrationDataList = new LinkedList<SubscriptionMigrationData>();
+
+ final List<TransferCancelData> transferCancelDataList = new LinkedList<TransferCancelData>();
+
+ DateTime bundleStartdate = null;
+
+ for (final SubscriptionBaseTimeline cur : bundleBaseTimeline.getSubscriptions()) {
+ final DefaultSubscriptionBase oldSubscription = (DefaultSubscriptionBase) dao.getSubscriptionFromId(cur.getId(), fromInternalCallContext);
+ // Skip already cancelled subscriptions
+ if (oldSubscription.getState() == EntitlementState.CANCELLED) {
+ continue;
+ }
+ final List<ExistingEvent> existingEvents = cur.getExistingEvents();
+ final ProductCategory productCategory = existingEvents.get(0).getPlanPhaseSpecifier().getProductCategory();
+
+ // For future add-on cancellations, don't add a cancellation on disk right away (mirror the behavior
+ // on base plan cancellations, even though we don't support un-transfer today)
+ if (productCategory != ProductCategory.ADD_ON || cancelImmediately) {
+ // Create the cancelWithRequestedDate event on effectiveCancelDate
+ final DateTime effectiveCancelDate = !cancelImmediately && oldSubscription.getChargedThroughDate() != null &&
+ effectiveTransferDate.isBefore(oldSubscription.getChargedThroughDate()) ?
+ oldSubscription.getChargedThroughDate() : effectiveTransferDate;
+
+ final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
+ .setSubscriptionId(cur.getId())
+ .setActiveVersion(cur.getActiveVersion())
+ .setProcessedDate(clock.getUTCNow())
+ .setEffectiveDate(effectiveCancelDate)
+ .setRequestedDate(effectiveTransferDate)
+ .setFromDisk(true));
+
+ TransferCancelData cancelData = new TransferCancelData(oldSubscription, cancelEvent);
+ transferCancelDataList.add(cancelData);
+ }
+
+ if (productCategory == ProductCategory.ADD_ON && !transferAddOn) {
+ continue;
+ }
+
+ // We Align with the original subscription
+ final DateTime subscriptionAlignStartDate = oldSubscription.getAlignStartDate();
+ if (bundleStartdate == null) {
+ bundleStartdate = oldSubscription.getStartDate();
+ }
+
+ // Create the new subscription for the new bundle on the new account
+ final DefaultSubscriptionBase defaultSubscriptionBase = createSubscriptionForApiUse(new SubscriptionBuilder()
+ .setId(UUID.randomUUID())
+ .setBundleId(subscriptionBundleData.getId())
+ .setCategory(productCategory)
+ .setBundleStartDate(effectiveTransferDate)
+ .setAlignStartDate(subscriptionAlignStartDate),
+ ImmutableList.<SubscriptionBaseEvent>of());
+
+ final List<SubscriptionBaseEvent> events = toEvents(existingEvents, defaultSubscriptionBase, effectiveTransferDate, context);
+ final SubscriptionMigrationData curData = new SubscriptionMigrationData(defaultSubscriptionBase, events, null);
+ subscriptionMigrationDataList.add(curData);
+ }
+ BundleMigrationData bundleMigrationData = new BundleMigrationData(subscriptionBundleData, subscriptionMigrationDataList);
+
+ // Atomically cancelWithRequestedDate all subscription on old account and create new bundle, subscriptions, events for new account
+ dao.transfer(sourceAccountId, destAccountId, bundleMigrationData, transferCancelDataList, fromInternalCallContext, toInternalCallContext);
+
+ return bundleMigrationData.getData();
+ } catch (SubscriptionBaseRepairException e) {
+ throw new SubscriptionBaseTransferApiException(e);
+ }
+ }
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/TransferCancelData.java b/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/TransferCancelData.java
new file mode 100644
index 0000000..b5deecb
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/transfer/TransferCancelData.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.transfer;
+
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+
+public class TransferCancelData {
+
+ final DefaultSubscriptionBase subscription;
+ final SubscriptionBaseEvent cancelEvent;
+
+ public TransferCancelData(final DefaultSubscriptionBase subscription,
+ final SubscriptionBaseEvent cancelEvent) {
+ this.subscription = subscription;
+ this.cancelEvent = cancelEvent;
+ }
+
+ public DefaultSubscriptionBase getSubscription() {
+ return subscription;
+ }
+
+ public SubscriptionBaseEvent getCancelEvent() {
+ return cancelEvent;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java
new file mode 100644
index 0000000..9d16160
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultEffectiveSubscriptionEvent.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultEffectiveSubscriptionEvent extends DefaultSubscriptionEvent implements EffectiveSubscriptionInternalEvent {
+
+ public DefaultEffectiveSubscriptionEvent(final SubscriptionBaseTransitionData in, final DateTime startDate, final UUID userToken, final Long accountRecordId, final Long tenantRecordId) {
+ super(in, startDate, accountRecordId, tenantRecordId, userToken);
+ }
+
+ @JsonCreator
+ public DefaultEffectiveSubscriptionEvent(@JsonProperty("eventId") final UUID eventId,
+ @JsonProperty("subscriptionId") final UUID subscriptionId,
+ @JsonProperty("bundleId") final UUID bundleId,
+ @JsonProperty("requestedTransitionTime") final DateTime requestedTransitionTime,
+ @JsonProperty("effectiveTransitionTime") final DateTime effectiveTransitionTime,
+ @JsonProperty("previousState") final EntitlementState previousState,
+ @JsonProperty("previousPlan") final String previousPlan,
+ @JsonProperty("previousPhase") final String previousPhase,
+ @JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("nextState") final EntitlementState nextState,
+ @JsonProperty("nextPlan") final String nextPlan,
+ @JsonProperty("nextPhase") final String nextPhase,
+ @JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("totalOrdering") final Long totalOrdering,
+ @JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
+ @JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
+ @JsonProperty("startDate") final DateTime startDate,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(eventId, subscriptionId, bundleId, requestedTransitionTime, effectiveTransitionTime, previousState, previousPlan,
+ previousPhase, previousPriceList, nextState, nextPlan, nextPhase, nextPriceList, totalOrdering,
+ transitionType, remainingEventsForUserOperation, startDate, searchKey1, searchKey2, userToken);
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java
new file mode 100644
index 0000000..b06bb90
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultRequestedSubscriptionEvent.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.events.RequestedSubscriptionInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultRequestedSubscriptionEvent extends DefaultSubscriptionEvent implements RequestedSubscriptionInternalEvent {
+
+ @JsonCreator
+ public DefaultRequestedSubscriptionEvent(@JsonProperty("eventId") final UUID eventId,
+ @JsonProperty("subscriptionId") final UUID subscriptionId,
+ @JsonProperty("bundleId") final UUID bundleId,
+ @JsonProperty("requestedTransitionTime") final DateTime requestedTransitionTime,
+ @JsonProperty("effectiveTransitionTime") final DateTime effectiveTransitionTime,
+ @JsonProperty("previousState") final EntitlementState previousState,
+ @JsonProperty("previousPlan") final String previousPlan,
+ @JsonProperty("previousPhase") final String previousPhase,
+ @JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("nextState") final EntitlementState nextState,
+ @JsonProperty("nextPlan") final String nextPlan,
+ @JsonProperty("nextPhase") final String nextPhase,
+ @JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("totalOrdering") final Long totalOrdering,
+ @JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
+ @JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
+ @JsonProperty("startDate") final DateTime startDate,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(eventId, subscriptionId, bundleId, requestedTransitionTime, effectiveTransitionTime, previousState, previousPlan,
+ previousPhase, previousPriceList, nextState, nextPlan, nextPhase, nextPriceList, totalOrdering,
+ transitionType, remainingEventsForUserOperation, startDate, searchKey1, searchKey2, userToken);
+ }
+
+ public DefaultRequestedSubscriptionEvent(final DefaultSubscriptionBase subscription,
+ final SubscriptionBaseEvent nextEvent,
+ final Long searchKey1,
+ final Long searchKey2,
+ final UUID userToken) {
+ this(nextEvent.getId(), nextEvent.getSubscriptionId(), subscription.getBundleId(), nextEvent.getRequestedDate(), nextEvent.getEffectiveDate(),
+ null, null, null, null, null, null, null, null, nextEvent.getTotalOrdering(), null, 0, null, searchKey1, searchKey2, userToken);
+ }
+}
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
new file mode 100644
index 0000000..285899c
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
@@ -0,0 +1,659 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementSourceType;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionDataIterator.Kind;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionDataIterator.Order;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionDataIterator.TimeLimit;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionDataIterator.Visibility;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.phase.PhaseEvent;
+import org.killbill.billing.subscription.events.user.ApiEvent;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.entity.EntityBase;
+
+public class DefaultSubscriptionBase extends EntityBase implements SubscriptionBase {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultSubscriptionBase.class);
+
+ private final Clock clock;
+ private final SubscriptionBaseApiService apiService;
+
+ //
+ // Final subscription fields
+ //
+ private final UUID bundleId;
+ private final DateTime alignStartDate;
+ private final DateTime bundleStartDate;
+ private final ProductCategory category;
+
+ //
+ // Those can be modified through non User APIs, and a new SubscriptionBase
+ // object would be created
+ //
+ private final long activeVersion;
+ private final DateTime chargedThroughDate;
+
+ //
+ // User APIs (create, change, cancelWithRequestedDate,...) will recompute those each time,
+ // so the user holding that subscription object get the correct state when
+ // the call completes
+ //
+ private LinkedList<SubscriptionBaseTransition> transitions;
+
+ // Low level events are ONLY used for Repair APIs
+ protected List<SubscriptionBaseEvent> events;
+
+
+ public List<SubscriptionBaseEvent> getEvents() {
+ return events;
+ }
+
+ // Transient object never returned at the API
+ public DefaultSubscriptionBase(final SubscriptionBuilder builder) {
+ this(builder, null, null);
+ }
+
+ public DefaultSubscriptionBase(final SubscriptionBuilder builder, @Nullable final SubscriptionBaseApiService apiService, @Nullable final Clock clock) {
+ super(builder.getId(), builder.getCreatedDate(), builder.getUpdatedDate());
+ this.apiService = apiService;
+ this.clock = clock;
+ this.bundleId = builder.getBundleId();
+ this.alignStartDate = builder.getAlignStartDate();
+ this.bundleStartDate = builder.getBundleStartDate();
+ this.category = builder.getCategory();
+ this.activeVersion = builder.getActiveVersion();
+ this.chargedThroughDate = builder.getChargedThroughDate();
+ }
+
+ // Used for API to make sure we have a clock and an apiService set before we return the object
+ public DefaultSubscriptionBase(final DefaultSubscriptionBase internalSubscription, final SubscriptionBaseApiService apiService, final Clock clock) {
+ super(internalSubscription.getId(), internalSubscription.getCreatedDate(), internalSubscription.getUpdatedDate());
+ this.apiService = apiService;
+ this.clock = clock;
+ this.bundleId = internalSubscription.getBundleId();
+ this.alignStartDate = internalSubscription.getAlignStartDate();
+ this.bundleStartDate = internalSubscription.getBundleStartDate();
+ this.category = internalSubscription.getCategory();
+ this.activeVersion = internalSubscription.getActiveVersion();
+ this.chargedThroughDate = internalSubscription.getChargedThroughDate();
+ this.transitions = new LinkedList<SubscriptionBaseTransition>(internalSubscription.getAllTransitions());
+ this.events = internalSubscription.getEvents();
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public DateTime getStartDate() {
+ return transitions.get(0).getEffectiveTransitionTime();
+ }
+
+ @Override
+ public EntitlementState getState() {
+ return (getPreviousTransition() == null) ? null
+ : getPreviousTransition().getNextState();
+ }
+
+ @Override
+ public EntitlementSourceType getSourceType() {
+ if (transitions == null) {
+ return null;
+ }
+ final SubscriptionBaseTransitionData initialTransition = (SubscriptionBaseTransitionData) transitions.get(0);
+ switch (initialTransition.getApiEventType()) {
+ case MIGRATE_BILLING:
+ case MIGRATE_ENTITLEMENT:
+ return EntitlementSourceType.MIGRATED;
+ case TRANSFER:
+ return EntitlementSourceType.TRANSFERRED;
+ default:
+ return EntitlementSourceType.NATIVE;
+ }
+ }
+
+ @Override
+ public PlanPhase getCurrentPhase() {
+ return (getPreviousTransition() == null) ? null
+ : getPreviousTransition().getNextPhase();
+ }
+
+ @Override
+ public Plan getCurrentPlan() {
+ return (getPreviousTransition() == null) ? null
+ : getPreviousTransition().getNextPlan();
+ }
+
+ @Override
+ public PriceList getCurrentPriceList() {
+ return (getPreviousTransition() == null) ? null :
+ getPreviousTransition().getNextPriceList();
+
+ }
+
+ @Override
+ public DateTime getEndDate() {
+ final SubscriptionBaseTransition latestTransition = getPreviousTransition();
+ if (latestTransition.getNextState() == EntitlementState.CANCELLED) {
+ return latestTransition.getEffectiveTransitionTime();
+ }
+ return null;
+ }
+
+ @Override
+ public DateTime getFutureEndDate() {
+ if (transitions == null) {
+ return null;
+ }
+
+ final SubscriptionBaseTransitionDataIterator it = new SubscriptionBaseTransitionDataIterator(
+ clock, transitions, Order.ASC_FROM_PAST, Kind.SUBSCRIPTION,
+ Visibility.ALL, TimeLimit.FUTURE_ONLY);
+ while (it.hasNext()) {
+ final SubscriptionBaseTransition cur = it.next();
+ if (cur.getTransitionType() == SubscriptionBaseTransitionType.CANCEL) {
+ return cur.getEffectiveTransitionTime();
+ }
+ }
+ return null;
+ }
+
+ public boolean recreate(final PlanPhaseSpecifier spec, final DateTime requestedDate,
+ final CallContext context) throws SubscriptionBaseApiException {
+ return apiService.recreatePlan(this, spec, requestedDate, context);
+ }
+
+ @Override
+ public boolean cancel(final CallContext context) throws SubscriptionBaseApiException {
+ return apiService.cancel(this, context);
+ }
+
+ @Override
+ public boolean cancelWithDate(final DateTime requestedDate, final CallContext context) throws SubscriptionBaseApiException {
+ return apiService.cancelWithRequestedDate(this, requestedDate, context);
+ }
+
+ @Override
+ public boolean cancelWithPolicy(final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+ return apiService.cancelWithPolicy(this, policy, context);
+ }
+
+ @Override
+ public boolean uncancel(final CallContext context)
+ throws SubscriptionBaseApiException {
+ return apiService.uncancel(this, context);
+ }
+
+ @Override
+ public DateTime changePlan(final String productName, final BillingPeriod term, final String priceList,
+ final CallContext context) throws SubscriptionBaseApiException {
+ return apiService.changePlan(this, productName, term, priceList, context);
+ }
+
+ @Override
+ public DateTime changePlanWithDate(final String productName, final BillingPeriod term, final String priceList,
+ final DateTime requestedDate, final CallContext context) throws SubscriptionBaseApiException {
+ return apiService.changePlanWithRequestedDate(this, productName, term, priceList, requestedDate, context);
+ }
+
+ @Override
+ public DateTime changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+ final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+ return apiService.changePlanWithPolicy(this, productName, term, priceList, policy, context);
+ }
+
+ @Override
+ public SubscriptionBaseTransition getPendingTransition() {
+ if (transitions == null) {
+ return null;
+ }
+ final SubscriptionBaseTransitionDataIterator it = new SubscriptionBaseTransitionDataIterator(
+ clock, transitions, Order.ASC_FROM_PAST, Kind.SUBSCRIPTION,
+ Visibility.ALL, TimeLimit.FUTURE_ONLY);
+ return it.hasNext() ? it.next() : null;
+ }
+
+ @Override
+ public Product getLastActiveProduct() {
+ if (getState() == EntitlementState.CANCELLED) {
+ final SubscriptionBaseTransition data = getPreviousTransition();
+ return data.getPreviousPlan().getProduct();
+ } else {
+ return getCurrentPlan().getProduct();
+ }
+ }
+
+ @Override
+ public PriceList getLastActivePriceList() {
+ if (getState() == EntitlementState.CANCELLED) {
+ final SubscriptionBaseTransition data = getPreviousTransition();
+ return data.getPreviousPriceList();
+ } else {
+ return getCurrentPriceList();
+ }
+ }
+
+ @Override
+ public ProductCategory getLastActiveCategory() {
+ if (getState() == EntitlementState.CANCELLED) {
+ final SubscriptionBaseTransition data = getPreviousTransition();
+ return data.getPreviousPlan().getProduct().getCategory();
+ } else {
+ return getCurrentPlan().getProduct().getCategory();
+ }
+ }
+
+ @Override
+ public Plan getLastActivePlan() {
+ if (getState() == EntitlementState.CANCELLED) {
+ final SubscriptionBaseTransition data = getPreviousTransition();
+ return data.getPreviousPlan();
+ } else {
+ return getCurrentPlan();
+ }
+ }
+
+ @Override
+ public PlanPhase getLastActivePhase() {
+ if (getState() == EntitlementState.CANCELLED) {
+ final SubscriptionBaseTransition data = getPreviousTransition();
+ return data.getPreviousPhase();
+ } else {
+ return getCurrentPhase();
+ }
+ }
+
+ @Override
+ public BillingPeriod getLastActiveBillingPeriod() {
+ if (getState() == EntitlementState.CANCELLED) {
+ final SubscriptionBaseTransition data = getPreviousTransition();
+ return data.getPreviousPlan().getBillingPeriod();
+ } else {
+ return getCurrentPlan().getBillingPeriod();
+ }
+ }
+
+ @Override
+ public SubscriptionBaseTransition getPreviousTransition() {
+ if (transitions == null) {
+ return null;
+ }
+ final SubscriptionBaseTransitionDataIterator it = new SubscriptionBaseTransitionDataIterator(
+ clock, transitions, Order.DESC_FROM_FUTURE, Kind.SUBSCRIPTION,
+ Visibility.FROM_DISK_ONLY, TimeLimit.PAST_OR_PRESENT_ONLY);
+ return it.hasNext() ? it.next() : null;
+ }
+
+ @Override
+ public ProductCategory getCategory() {
+ return category;
+ }
+
+ public DateTime getBundleStartDate() {
+ return bundleStartDate;
+ }
+
+ @Override
+ public DateTime getChargedThroughDate() {
+ return chargedThroughDate;
+ }
+
+ @Override
+ public List<SubscriptionBaseTransition> getAllTransitions() {
+ if (transitions == null) {
+ return Collections.emptyList();
+ }
+ final List<SubscriptionBaseTransition> result = new ArrayList<SubscriptionBaseTransition>();
+ final SubscriptionBaseTransitionDataIterator it = new SubscriptionBaseTransitionDataIterator(clock, transitions, Order.ASC_FROM_PAST, Kind.ALL, Visibility.ALL, TimeLimit.ALL);
+ while (it.hasNext()) {
+ result.add(it.next());
+ }
+ return result;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((id == null) ? 0 : id.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final DefaultSubscriptionBase other = (DefaultSubscriptionBase) obj;
+ if (id == null) {
+ if (other.id != null) {
+ return false;
+ }
+ } else if (!id.equals(other.id)) {
+ return false;
+ }
+ return true;
+ }
+
+
+ public SubscriptionBaseTransitionData getTransitionFromEvent(final SubscriptionBaseEvent event, final int seqId) {
+ if (transitions == null || event == null) {
+ return null;
+ }
+ SubscriptionBaseTransitionData prev = null;
+ for (final SubscriptionBaseTransition cur : transitions) {
+ final SubscriptionBaseTransitionData curData = (SubscriptionBaseTransitionData) cur;
+ if (curData.getId().equals(event.getId())) {
+
+ final SubscriptionBaseTransitionData withSeq = new SubscriptionBaseTransitionData(curData, seqId);
+ return withSeq;
+ }
+ if (curData.getTotalOrdering() < event.getTotalOrdering()) {
+ prev = curData;
+ }
+ }
+ // Since UNCANCEL are not part of the transitions, we compute a new 'UNCANCEL' transition based on the event right before that UNCANCEL
+ // This is used to be able to send a bus event for uncancellation
+ if (prev != null && event.getType() == EventType.API_USER && ((ApiEvent) event).getEventType() == ApiEventType.UNCANCEL) {
+ final SubscriptionBaseTransitionData withSeq = new SubscriptionBaseTransitionData((SubscriptionBaseTransitionData) prev, EventType.API_USER, ApiEventType.UNCANCEL, seqId);
+ return withSeq;
+ }
+ return null;
+ }
+
+ public DateTime getAlignStartDate() {
+ return alignStartDate;
+ }
+
+ public long getLastEventOrderedId() {
+ final SubscriptionBaseTransitionDataIterator it = new SubscriptionBaseTransitionDataIterator(
+ clock, transitions, Order.DESC_FROM_FUTURE, Kind.SUBSCRIPTION,
+ Visibility.FROM_DISK_ONLY, TimeLimit.ALL);
+ return it.hasNext() ? ((SubscriptionBaseTransitionData) it.next()).getTotalOrdering() : -1L;
+ }
+
+ public long getActiveVersion() {
+ return activeVersion;
+ }
+
+ public List<SubscriptionBaseTransition> getBillingTransitions() {
+
+ if (transitions == null) {
+ return Collections.emptyList();
+ }
+ final List<SubscriptionBaseTransition> result = new ArrayList<SubscriptionBaseTransition>();
+ final SubscriptionBaseTransitionDataIterator it = new SubscriptionBaseTransitionDataIterator(
+ clock, transitions, Order.ASC_FROM_PAST, Kind.BILLING,
+ Visibility.ALL, TimeLimit.ALL);
+ // Remove anything prior to first CREATE or MIGRATE_BILLING
+ boolean foundInitialEvent = false;
+ while (it.hasNext()) {
+ final SubscriptionBaseTransitionData curTransition = (SubscriptionBaseTransitionData) it.next();
+ if (!foundInitialEvent) {
+ foundInitialEvent = curTransition.getEventType() == EventType.API_USER &&
+ (curTransition.getApiEventType() == ApiEventType.CREATE ||
+ curTransition.getApiEventType() == ApiEventType.MIGRATE_BILLING ||
+ curTransition.getApiEventType() == ApiEventType.TRANSFER);
+ }
+ if (foundInitialEvent) {
+ result.add(curTransition);
+ }
+ }
+ return result;
+ }
+
+
+ public SubscriptionBaseTransitionData getInitialTransitionForCurrentPlan() {
+ if (transitions == null) {
+ throw new SubscriptionBaseError(String.format("No transitions for subscription %s", getId()));
+ }
+
+ final SubscriptionBaseTransitionDataIterator it = new SubscriptionBaseTransitionDataIterator(clock,
+ transitions,
+ Order.DESC_FROM_FUTURE,
+ Kind.SUBSCRIPTION,
+ Visibility.ALL,
+ TimeLimit.PAST_OR_PRESENT_ONLY);
+
+ while (it.hasNext()) {
+ final SubscriptionBaseTransitionData cur = (SubscriptionBaseTransitionData) it.next();
+ if (cur.getTransitionType() == SubscriptionBaseTransitionType.CREATE
+ || cur.getTransitionType() == SubscriptionBaseTransitionType.RE_CREATE
+ || cur.getTransitionType() == SubscriptionBaseTransitionType.TRANSFER
+ || cur.getTransitionType() == SubscriptionBaseTransitionType.CHANGE
+ || cur.getTransitionType() == SubscriptionBaseTransitionType.MIGRATE_ENTITLEMENT) {
+ return cur;
+ }
+ }
+
+ throw new SubscriptionBaseError(String.format("Failed to find InitialTransitionForCurrentPlan id = %s", getId()));
+ }
+
+ public boolean isSubscriptionFutureCancelled() {
+ return getFutureEndDate() != null;
+ }
+
+ public DateTime getPlanChangeEffectiveDate(final BillingActionPolicy policy) {
+ switch (policy) {
+ case IMMEDIATE:
+ return clock.getUTCNow();
+ case END_OF_TERM:
+ //
+ // If we have a chargedThroughDate that is 'up to date' we use it, if not default to now
+ // chargedThroughDate could exist and be less than now if:
+ // 1. account is not being invoiced, for e.g AUTO_INVOICING_OFF nis set
+ // 2. In the case if FIXED item CTD is set using startDate of the service period
+ //
+ return (chargedThroughDate != null && chargedThroughDate.isAfter(clock.getUTCNow())) ? chargedThroughDate : clock.getUTCNow();
+ default:
+ throw new SubscriptionBaseError(String.format(
+ "Unexpected policy type %s", policy.toString()));
+ }
+ }
+
+ public DateTime getCurrentPhaseStart() {
+
+ if (transitions == null) {
+ throw new SubscriptionBaseError(String.format(
+ "No transitions for subscription %s", getId()));
+ }
+ final SubscriptionBaseTransitionDataIterator it = new SubscriptionBaseTransitionDataIterator(
+ clock, transitions, Order.DESC_FROM_FUTURE, Kind.SUBSCRIPTION,
+ Visibility.ALL, TimeLimit.PAST_OR_PRESENT_ONLY);
+ while (it.hasNext()) {
+ final SubscriptionBaseTransitionData cur = (SubscriptionBaseTransitionData) it.next();
+
+ if (cur.getTransitionType() == SubscriptionBaseTransitionType.PHASE
+ || cur.getTransitionType() == SubscriptionBaseTransitionType.TRANSFER
+ || cur.getTransitionType() == SubscriptionBaseTransitionType.CREATE
+ || cur.getTransitionType() == SubscriptionBaseTransitionType.RE_CREATE
+ || cur.getTransitionType() == SubscriptionBaseTransitionType.CHANGE
+ || cur.getTransitionType() == SubscriptionBaseTransitionType.MIGRATE_ENTITLEMENT) {
+ return cur.getEffectiveTransitionTime();
+ }
+ }
+ throw new SubscriptionBaseError(String.format(
+ "Failed to find CurrentPhaseStart id = %s", getId().toString()));
+ }
+
+ public void rebuildTransitions(final List<SubscriptionBaseEvent> inputEvents, final Catalog catalog) {
+
+ if (inputEvents == null) {
+ return;
+ }
+
+ this.events = inputEvents;
+
+ UUID nextUserToken = null;
+
+ UUID nextEventId = null;
+ DateTime nextCreatedDate = null;
+ EntitlementState nextState = null;
+ String nextPlanName = null;
+ String nextPhaseName = null;
+ String nextPriceListName = null;
+
+ UUID prevEventId = null;
+ DateTime prevCreatedDate = null;
+ EntitlementState previousState = null;
+ PriceList previousPriceList = null;
+ Plan previousPlan = null;
+ PlanPhase previousPhase = null;
+
+ transitions = new LinkedList<SubscriptionBaseTransition>();
+
+ for (final SubscriptionBaseEvent cur : inputEvents) {
+
+ if (!cur.isActive() || cur.getActiveVersion() < activeVersion) {
+ continue;
+ }
+
+ ApiEventType apiEventType = null;
+
+ boolean isFromDisk = true;
+
+ nextEventId = cur.getId();
+ nextCreatedDate = cur.getCreatedDate();
+
+ switch (cur.getType()) {
+
+ case PHASE:
+ final PhaseEvent phaseEV = (PhaseEvent) cur;
+ nextPhaseName = phaseEV.getPhase();
+ break;
+
+ case API_USER:
+ final ApiEvent userEV = (ApiEvent) cur;
+ apiEventType = userEV.getEventType();
+ isFromDisk = userEV.isFromDisk();
+
+ switch (apiEventType) {
+ case TRANSFER:
+ case MIGRATE_BILLING:
+ case MIGRATE_ENTITLEMENT:
+ case CREATE:
+ case RE_CREATE:
+ prevEventId = null;
+ prevCreatedDate = null;
+ previousState = null;
+ previousPlan = null;
+ previousPhase = null;
+ previousPriceList = null;
+ nextState = EntitlementState.ACTIVE;
+ nextPlanName = userEV.getEventPlan();
+ nextPhaseName = userEV.getEventPlanPhase();
+ nextPriceListName = userEV.getPriceList();
+ break;
+ case CHANGE:
+ nextPlanName = userEV.getEventPlan();
+ nextPhaseName = userEV.getEventPlanPhase();
+ nextPriceListName = userEV.getPriceList();
+ break;
+ case CANCEL:
+ nextState = EntitlementState.CANCELLED;
+ nextPlanName = null;
+ nextPhaseName = null;
+ break;
+ case UNCANCEL:
+ default:
+ throw new SubscriptionBaseError(String.format(
+ "Unexpected UserEvent type = %s", userEV
+ .getEventType().toString()));
+ }
+ break;
+ default:
+ throw new SubscriptionBaseError(String.format(
+ "Unexpected Event type = %s", cur.getType()));
+ }
+
+ Plan nextPlan = null;
+ PlanPhase nextPhase = null;
+ PriceList nextPriceList = null;
+
+ try {
+ nextPlan = (nextPlanName != null) ? catalog.findPlan(nextPlanName, cur.getRequestedDate(), getAlignStartDate()) : null;
+ nextPhase = (nextPhaseName != null) ? catalog.findPhase(nextPhaseName, cur.getRequestedDate(), getAlignStartDate()) : null;
+ nextPriceList = (nextPriceListName != null) ? catalog.findPriceList(nextPriceListName, cur.getRequestedDate()) : null;
+ } catch (CatalogApiException e) {
+ log.error(String.format("Failed to build transition for subscription %s", id), e);
+ }
+
+ final SubscriptionBaseTransitionData transition = new SubscriptionBaseTransitionData(
+ cur.getId(), id, bundleId, cur.getType(), apiEventType,
+ cur.getRequestedDate(), cur.getEffectiveDate(),
+ prevEventId, prevCreatedDate,
+ previousState, previousPlan, previousPhase,
+ previousPriceList,
+ nextEventId, nextCreatedDate,
+ nextState, nextPlan, nextPhase,
+ nextPriceList, cur.getTotalOrdering(),
+ cur.getCreatedDate(),
+ nextUserToken,
+ isFromDisk);
+
+ transitions.add(transition);
+
+ previousState = nextState;
+ previousPlan = nextPlan;
+ previousPhase = nextPhase;
+ previousPriceList = nextPriceList;
+ prevEventId = nextEventId;
+ prevCreatedDate = nextCreatedDate;
+
+ }
+ }
+}
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
new file mode 100644
index 0000000..ba1ea44
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+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.PlanPhase;
+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.PriceListSet;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.clock.Clock;
+import org.killbill.clock.DefaultClock;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.alignment.PlanAligner;
+import org.killbill.billing.subscription.alignment.TimedPhase;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.engine.addon.AddonUtils;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.phase.PhaseEvent;
+import org.killbill.billing.subscription.events.phase.PhaseEventData;
+import org.killbill.billing.subscription.events.user.ApiEvent;
+import org.killbill.billing.subscription.events.user.ApiEventBuilder;
+import org.killbill.billing.subscription.events.user.ApiEventCancel;
+import org.killbill.billing.subscription.events.user.ApiEventChange;
+import org.killbill.billing.subscription.events.user.ApiEventCreate;
+import org.killbill.billing.subscription.events.user.ApiEventReCreate;
+import org.killbill.billing.subscription.events.user.ApiEventUncancel;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+
+import com.google.inject.Inject;
+
+public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiService {
+
+ private final Clock clock;
+ private final SubscriptionDao dao;
+ private final CatalogService catalogService;
+ private final PlanAligner planAligner;
+ private final AddonUtils addonUtils;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public DefaultSubscriptionBaseApiService(final Clock clock, final SubscriptionDao dao, final CatalogService catalogService,
+ final PlanAligner planAligner, final AddonUtils addonUtils,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.clock = clock;
+ this.catalogService = catalogService;
+ this.planAligner = planAligner;
+ this.dao = dao;
+ this.addonUtils = addonUtils;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public DefaultSubscriptionBase createPlan(final SubscriptionBuilder builder, final Plan plan, final PhaseType initialPhase,
+ final String realPriceList, final DateTime requestedDate, final DateTime effectiveDate, final DateTime processedDate,
+ final CallContext context) throws SubscriptionBaseApiException {
+ final DefaultSubscriptionBase subscription = new DefaultSubscriptionBase(builder, this, clock);
+
+ createFromSubscription(subscription, plan, initialPhase, realPriceList, requestedDate, effectiveDate, processedDate, false, context);
+ return subscription;
+ }
+
+ @Deprecated
+ @Override
+ public boolean recreatePlan(final DefaultSubscriptionBase subscription, final PlanPhaseSpecifier spec, final DateTime requestedDateWithMs, final CallContext context)
+ throws SubscriptionBaseApiException {
+ final EntitlementState currentState = subscription.getState();
+ if (currentState != null && currentState != EntitlementState.CANCELLED) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_RECREATE_BAD_STATE, subscription.getId(), currentState);
+ }
+
+ final DateTime now = clock.getUTCNow();
+ final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
+ validateEffectiveDate(subscription, effectiveDate);
+
+ try {
+ final String realPriceList = (spec.getPriceListName() == null) ? PriceListSet.DEFAULT_PRICELIST_NAME : spec.getPriceListName();
+ final Plan plan = catalogService.getFullCatalog().findPlan(spec.getProductName(), spec.getBillingPeriod(), realPriceList, effectiveDate);
+ final PlanPhase phase = plan.getAllPhases()[0];
+ if (phase == null) {
+ throw new SubscriptionBaseError(String.format("No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
+ spec.getProductName(), spec.getBillingPeriod().toString(), realPriceList));
+ }
+
+ final DateTime processedDate = now;
+
+ createFromSubscription(subscription, plan, spec.getPhaseType(), realPriceList, now, effectiveDate, processedDate, true, context);
+ return true;
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+ }
+
+ private void createFromSubscription(final DefaultSubscriptionBase subscription, final Plan plan, final PhaseType initialPhase,
+ final String realPriceList, final DateTime requestedDate, final DateTime effectiveDate, final DateTime processedDate,
+ final boolean reCreate, final CallContext context) throws SubscriptionBaseApiException {
+ final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
+
+ try {
+ final TimedPhase[] curAndNextPhases = planAligner.getCurrentAndNextTimedPhaseOnCreate(subscription, plan, initialPhase, realPriceList, requestedDate, effectiveDate);
+
+ final ApiEventBuilder createBuilder = new ApiEventBuilder()
+ .setSubscriptionId(subscription.getId())
+ .setEventPlan(plan.getName())
+ .setEventPlanPhase(curAndNextPhases[0].getPhase().getName())
+ .setEventPriceList(realPriceList)
+ .setActiveVersion(subscription.getActiveVersion())
+ .setProcessedDate(processedDate)
+ .setEffectiveDate(effectiveDate)
+ .setRequestedDate(requestedDate)
+ .setFromDisk(true);
+ final ApiEvent creationEvent = (reCreate) ? new ApiEventReCreate(createBuilder) : new ApiEventCreate(createBuilder);
+
+ final TimedPhase nextTimedPhase = curAndNextPhases[1];
+ final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+ PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, processedDate, nextTimedPhase.getStartPhase()) :
+ null;
+ final List<SubscriptionBaseEvent> events = new ArrayList<SubscriptionBaseEvent>();
+ events.add(creationEvent);
+ if (nextPhaseEvent != null) {
+ events.add(nextPhaseEvent);
+ }
+ if (reCreate) {
+ dao.recreateSubscription(subscription, events, internalCallContext);
+ } else {
+ dao.createSubscription(subscription, events, internalCallContext);
+ }
+ subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), catalogService.getFullCatalog());
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+ }
+
+ @Override
+ public boolean cancel(final DefaultSubscriptionBase subscription, final CallContext context) throws SubscriptionBaseApiException {
+
+ final EntitlementState currentState = subscription.getState();
+ if (currentState != EntitlementState.ACTIVE) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CANCEL_BAD_STATE, subscription.getId(), currentState);
+ }
+ final DateTime now = clock.getUTCNow();
+
+ final Plan currentPlan = subscription.getCurrentPlan();
+ final PlanPhaseSpecifier planPhase = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
+ currentPlan.getProduct().getCategory(),
+ subscription.getCurrentPlan().getBillingPeriod(),
+ subscription.getCurrentPriceList().getName(),
+ subscription.getCurrentPhase().getPhaseType());
+
+ try {
+ final BillingActionPolicy policy = catalogService.getFullCatalog().planCancelPolicy(planPhase, now);
+ final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
+
+ return doCancelPlan(subscription, now, effectiveDate, context);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+ }
+
+ @Override
+ public boolean cancelWithRequestedDate(final DefaultSubscriptionBase subscription, final DateTime requestedDateWithMs, final CallContext context) throws SubscriptionBaseApiException {
+ final EntitlementState currentState = subscription.getState();
+ if (currentState != EntitlementState.ACTIVE) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CANCEL_BAD_STATE, subscription.getId(), currentState);
+ }
+ final DateTime now = clock.getUTCNow();
+ final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
+ return doCancelPlan(subscription, now, effectiveDate, context);
+ }
+
+ @Override
+ public boolean cancelWithPolicy(final DefaultSubscriptionBase subscription, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+ final EntitlementState currentState = subscription.getState();
+ if (currentState != EntitlementState.ACTIVE) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CANCEL_BAD_STATE, subscription.getId(), currentState);
+ }
+ final DateTime now = clock.getUTCNow();
+ final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
+
+ return doCancelPlan(subscription, now, effectiveDate, context);
+ }
+
+ private boolean doCancelPlan(final DefaultSubscriptionBase subscription, final DateTime now, final DateTime effectiveDate, final CallContext context) throws SubscriptionBaseApiException {
+ validateEffectiveDate(subscription, effectiveDate);
+
+ final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
+ .setSubscriptionId(subscription.getId())
+ .setActiveVersion(subscription.getActiveVersion())
+ .setProcessedDate(now)
+ .setEffectiveDate(effectiveDate)
+ .setRequestedDate(now)
+ .setFromDisk(true));
+
+ final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
+ dao.cancelSubscription(subscription, cancelEvent, internalCallContext, 0);
+ subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), catalogService.getFullCatalog());
+
+ if (subscription.getCategory() == ProductCategory.BASE) {
+ cancelAddOnsIfRequired(subscription, effectiveDate, internalCallContext);
+ }
+
+ final boolean isImmediate = subscription.getState() == EntitlementState.CANCELLED;
+ return isImmediate;
+ }
+
+ @Override
+ public boolean uncancel(final DefaultSubscriptionBase subscription, final CallContext context) throws SubscriptionBaseApiException {
+ if (!subscription.isSubscriptionFutureCancelled()) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_UNCANCEL_BAD_STATE, subscription.getId().toString());
+ }
+
+ final DateTime now = clock.getUTCNow();
+ final SubscriptionBaseEvent uncancelEvent = new ApiEventUncancel(new ApiEventBuilder()
+ .setSubscriptionId(subscription.getId())
+ .setActiveVersion(subscription.getActiveVersion())
+ .setProcessedDate(now)
+ .setRequestedDate(now)
+ .setEffectiveDate(now)
+ .setFromDisk(true));
+
+ final List<SubscriptionBaseEvent> uncancelEvents = new ArrayList<SubscriptionBaseEvent>();
+ uncancelEvents.add(uncancelEvent);
+
+ final TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription, now, now);
+ final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+ PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+ null;
+ if (nextPhaseEvent != null) {
+ uncancelEvents.add(nextPhaseEvent);
+ }
+
+ final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
+ dao.uncancelSubscription(subscription, uncancelEvents, internalCallContext);
+ subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), catalogService.getFullCatalog());
+
+ return true;
+ }
+
+ @Override
+ public DateTime changePlan(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
+ final String priceList, final CallContext context) throws SubscriptionBaseApiException {
+ final DateTime now = clock.getUTCNow();
+
+ validateEntitlementState(subscription);
+
+ final PlanChangeResult planChangeResult = getPlanChangeResult(subscription, productName, term, priceList, now);
+ final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(planChangeResult.getPolicy());
+ validateEffectiveDate(subscription, effectiveDate);
+
+ try {
+ return doChangePlan(subscription, productName, term, planChangeResult.getNewPriceList().getName(), now, effectiveDate, context);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+ }
+
+ @Override
+ public DateTime changePlanWithRequestedDate(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
+ final String priceList, final DateTime requestedDateWithMs, final CallContext context) throws SubscriptionBaseApiException {
+ final DateTime now = clock.getUTCNow();
+ final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
+
+ validateEffectiveDate(subscription, effectiveDate);
+ validateEntitlementState(subscription);
+
+ try {
+ return doChangePlan(subscription, productName, term, priceList, now, effectiveDate, context);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+ }
+
+ @Override
+ public DateTime changePlanWithPolicy(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
+ final String priceList, final BillingActionPolicy policy, final CallContext context)
+ throws SubscriptionBaseApiException {
+ final DateTime now = clock.getUTCNow();
+
+ validateEntitlementState(subscription);
+
+ final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
+ try {
+ return doChangePlan(subscription, productName, term, priceList, now, effectiveDate, context);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+ }
+
+ private PlanChangeResult getPlanChangeResult(final DefaultSubscriptionBase subscription, final String productName,
+ final BillingPeriod term, final String priceList, final DateTime effectiveDate) throws SubscriptionBaseApiException {
+ final PlanChangeResult planChangeResult;
+ try {
+ final Product destProduct = catalogService.getFullCatalog().findProduct(productName, effectiveDate);
+ final Plan currentPlan = subscription.getCurrentPlan();
+ final PriceList currentPriceList = subscription.getCurrentPriceList();
+ final PlanPhaseSpecifier fromPlanPhase = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
+ currentPlan.getProduct().getCategory(),
+ currentPlan.getBillingPeriod(),
+ currentPriceList.getName(),
+ subscription.getCurrentPhase().getPhaseType());
+ final PlanSpecifier toPlanPhase = new PlanSpecifier(productName,
+ destProduct.getCategory(),
+ term,
+ priceList);
+
+ planChangeResult = catalogService.getFullCatalog().planChange(fromPlanPhase, toPlanPhase, effectiveDate);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseApiException(e);
+ }
+
+ return planChangeResult;
+ }
+
+ private DateTime doChangePlan(final DefaultSubscriptionBase subscription,
+ final String newProductName,
+ final BillingPeriod newBillingPeriod,
+ final String newPriceList,
+ final DateTime now,
+ final DateTime effectiveDate,
+ final CallContext context) throws SubscriptionBaseApiException, CatalogApiException {
+
+ final Plan newPlan = catalogService.getFullCatalog().findPlan(newProductName, newBillingPeriod, newPriceList, effectiveDate, subscription.getStartDate());
+
+ final TimedPhase currentTimedPhase = planAligner.getCurrentTimedPhaseOnChange(subscription, newPlan, newPriceList, now, effectiveDate);
+
+ final SubscriptionBaseEvent changeEvent = new ApiEventChange(new ApiEventBuilder()
+ .setSubscriptionId(subscription.getId())
+ .setEventPlan(newPlan.getName())
+ .setEventPlanPhase(currentTimedPhase.getPhase().getName())
+ .setEventPriceList(newPriceList)
+ .setActiveVersion(subscription.getActiveVersion())
+ .setProcessedDate(now)
+ .setEffectiveDate(effectiveDate)
+ .setRequestedDate(now)
+ .setFromDisk(true));
+
+ final TimedPhase nextTimedPhase = planAligner.getNextTimedPhaseOnChange(subscription, newPlan, newPriceList, now, effectiveDate);
+ final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+ PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+ null;
+
+ final List<SubscriptionBaseEvent> changeEvents = new ArrayList<SubscriptionBaseEvent>();
+ // Only add the PHASE if it does not coincide with the CHANGE, if not this is 'just' a CHANGE.
+ if (nextPhaseEvent != null && !nextPhaseEvent.getEffectiveDate().equals(changeEvent.getEffectiveDate())) {
+ changeEvents.add(nextPhaseEvent);
+ }
+ changeEvents.add(changeEvent);
+
+ final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
+ dao.changePlan(subscription, changeEvents, internalCallContext);
+ subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), catalogService.getFullCatalog());
+
+ if (subscription.getCategory() == ProductCategory.BASE) {
+ cancelAddOnsIfRequired(subscription, effectiveDate, internalCallContext);
+ }
+
+ final boolean isChangeImmediate = subscription.getCurrentPlan().getProduct().getName().equals(newProductName) &&
+ subscription.getCurrentPlan().getBillingPeriod() == newBillingPeriod;
+ return effectiveDate;
+ }
+
+ public int cancelAddOnsIfRequired(final DefaultSubscriptionBase baseSubscription, final DateTime effectiveDate, final InternalCallContext context) {
+
+ // If cancellation/change occur in the future, there is nothing to do
+ final DateTime now = clock.getUTCNow();
+ if (effectiveDate.compareTo(now) > 0) {
+ return 0;
+ }
+
+ final Product baseProduct = (baseSubscription.getState() == EntitlementState.CANCELLED) ? null : baseSubscription.getCurrentPlan().getProduct();
+
+ final List<SubscriptionBase> subscriptions = dao.getSubscriptions(baseSubscription.getBundleId(), context);
+
+ final List<DefaultSubscriptionBase> subscriptionsToBeCancelled = new LinkedList<DefaultSubscriptionBase>();
+ final List<SubscriptionBaseEvent> cancelEvents = new LinkedList<SubscriptionBaseEvent>();
+
+ for (final SubscriptionBase subscription : subscriptions) {
+ final DefaultSubscriptionBase cur = (DefaultSubscriptionBase) subscription;
+ if (cur.getState() == EntitlementState.CANCELLED ||
+ cur.getCategory() != ProductCategory.ADD_ON) {
+ continue;
+ }
+
+ final Plan addonCurrentPlan = cur.getCurrentPlan();
+ if (baseProduct == null ||
+ addonUtils.isAddonIncluded(baseProduct, addonCurrentPlan) ||
+ !addonUtils.isAddonAvailable(baseProduct, addonCurrentPlan)) {
+ //
+ // Perform AO cancellation using the effectiveDate of the BP
+ //
+ final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
+ .setSubscriptionId(cur.getId())
+ .setActiveVersion(cur.getActiveVersion())
+ .setProcessedDate(now)
+ .setEffectiveDate(effectiveDate)
+ .setRequestedDate(now)
+ .setFromDisk(true));
+ subscriptionsToBeCancelled.add(cur);
+ cancelEvents.add(cancelEvent);
+ }
+ }
+
+ dao.cancelSubscriptions(subscriptionsToBeCancelled, cancelEvents, context);
+ return subscriptionsToBeCancelled.size();
+ }
+
+ private void validateEffectiveDate(final DefaultSubscriptionBase subscription, final DateTime effectiveDate) throws SubscriptionBaseApiException {
+ final SubscriptionBaseTransition previousTransition = subscription.getPreviousTransition();
+ if (previousTransition != null && previousTransition.getEffectiveTransitionTime().isAfter(effectiveDate)) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_REQUESTED_DATE,
+ effectiveDate.toString(), previousTransition.getEffectiveTransitionTime());
+ }
+ }
+
+ private void validateEntitlementState(final DefaultSubscriptionBase subscription) throws SubscriptionBaseApiException {
+ final EntitlementState currentState = subscription.getState();
+ if (currentState != EntitlementState.ACTIVE) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_NON_ACTIVE, subscription.getId(), currentState);
+ }
+ if (subscription.isSubscriptionFutureCancelled()) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_FUTURE_CANCELLED, subscription.getId());
+ }
+ }
+
+ private InternalCallContext createCallContextFromBundleId(final UUID bundleId, final CallContext context) {
+ return internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, context);
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseBundle.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseBundle.java
new file mode 100644
index 0000000..dbef7be
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseBundle.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entity.EntityBase;
+
+public class DefaultSubscriptionBaseBundle extends EntityBase implements SubscriptionBaseBundle {
+
+ private final String key;
+ private final UUID accountId;
+ private final DateTime lastSysUpdateDate;
+ private final DateTime originalCreatedDate;
+
+ public DefaultSubscriptionBaseBundle(final String name, final UUID accountId, final DateTime startDate, final DateTime originalCreatedDate,
+ final DateTime createdDate, final DateTime updatedDate) {
+ this(UUID.randomUUID(), name, accountId, startDate, originalCreatedDate, createdDate, updatedDate);
+ }
+
+ public DefaultSubscriptionBaseBundle(final UUID id, final String key, final UUID accountId, final DateTime lastSysUpdate, final DateTime originalCreatedDate,
+ final DateTime createdDate, final DateTime updatedDate) {
+ super(id, createdDate, updatedDate);
+ this.key = key;
+ this.accountId = accountId;
+ this.lastSysUpdateDate = lastSysUpdate;
+ this.originalCreatedDate = originalCreatedDate;
+ }
+
+ @Override
+ public String getExternalKey() {
+ return key;
+ }
+
+ @Override
+ public DateTime getOriginalCreatedDate() {
+ return originalCreatedDate;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public DateTime getLastSysUpdateDate() {
+ return lastSysUpdateDate;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultSubscriptionBaseBundle");
+ sb.append("{accountId=").append(accountId);
+ sb.append(", id=").append(id);
+ sb.append(", key='").append(key).append('\'');
+ sb.append(", lastSysUpdateDate=").append(lastSysUpdateDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultSubscriptionBaseBundle that = (DefaultSubscriptionBaseBundle) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (id != null ? !id.equals(that.id) : that.id != null) {
+ return false;
+ }
+ if (key != null ? !key.equals(that.key) : that.key != null) {
+ return false;
+ }
+ if (lastSysUpdateDate != null ? !lastSysUpdateDate.equals(that.lastSysUpdateDate) : that.lastSysUpdateDate != null) {
+ return false;
+ }
+ if (originalCreatedDate != null ? !originalCreatedDate.equals(that.originalCreatedDate) : that.originalCreatedDate != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + (key != null ? key.hashCode() : 0);
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (lastSysUpdateDate != null ? lastSysUpdateDate.hashCode() : 0);
+ result = 31 * result + (originalCreatedDate != null ? originalCreatedDate.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java
new file mode 100644
index 0000000..8389665
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionEvent.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.SubscriptionInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public abstract class DefaultSubscriptionEvent extends BusEventBase implements SubscriptionInternalEvent {
+
+ private final Long totalOrdering;
+ private final UUID subscriptionId;
+ private final UUID bundleId;
+ private final UUID eventId;
+ private final DateTime requestedTransitionTime;
+ private final DateTime effectiveTransitionTime;
+ private final EntitlementState previousState;
+ private final String previousPriceList;
+ private final String previousPlan;
+ private final String previousPhase;
+ private final EntitlementState nextState;
+ private final String nextPriceList;
+ private final String nextPlan;
+ private final String nextPhase;
+ private final Integer remainingEventsForUserOperation;
+ private final SubscriptionBaseTransitionType transitionType;
+ private final DateTime startDate;
+
+ public DefaultSubscriptionEvent(final SubscriptionBaseTransitionData in, final DateTime startDate,
+ final Long searchKey1,
+ final Long searchKey2,
+ final UUID userToken) {
+ this(in.getId(),
+ in.getSubscriptionId(),
+ in.getBundleId(),
+ in.getRequestedTransitionTime(),
+ in.getEffectiveTransitionTime(),
+ in.getPreviousState(),
+ (in.getPreviousPlan() != null) ? in.getPreviousPlan().getName() : null,
+ (in.getPreviousPhase() != null) ? in.getPreviousPhase().getName() : null,
+ (in.getPreviousPriceList() != null) ? in.getPreviousPriceList().getName() : null,
+ in.getNextState(),
+ (in.getNextPlan() != null) ? in.getNextPlan().getName() : null,
+ (in.getNextPhase() != null) ? in.getNextPhase().getName() : null,
+ (in.getNextPriceList() != null) ? in.getNextPriceList().getName() : null,
+ in.getTotalOrdering(),
+ in.getTransitionType(),
+ in.getRemainingEventsForUserOperation(),
+ startDate,
+ searchKey1,
+ searchKey2,
+ userToken);
+ }
+
+ @JsonCreator
+ public DefaultSubscriptionEvent(@JsonProperty("eventId") final UUID eventId,
+ @JsonProperty("subscriptionId") final UUID subscriptionId,
+ @JsonProperty("bundleId") final UUID bundleId,
+ @JsonProperty("requestedTransitionTime") final DateTime requestedTransitionTime,
+ @JsonProperty("effectiveTransitionTime") final DateTime effectiveTransitionTime,
+ @JsonProperty("previousState") final EntitlementState previousState,
+ @JsonProperty("previousPlan") final String previousPlan,
+ @JsonProperty("previousPhase") final String previousPhase,
+ @JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("nextState") final EntitlementState nextState,
+ @JsonProperty("nextPlan") final String nextPlan,
+ @JsonProperty("nextPhase") final String nextPhase,
+ @JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("totalOrdering") final Long totalOrdering,
+ @JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
+ @JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
+ @JsonProperty("startDate") final DateTime startDate,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.eventId = eventId;
+ this.subscriptionId = subscriptionId;
+ this.bundleId = bundleId;
+ this.requestedTransitionTime = requestedTransitionTime;
+ this.effectiveTransitionTime = effectiveTransitionTime;
+ this.previousState = previousState;
+ this.previousPriceList = previousPriceList;
+ this.previousPlan = previousPlan;
+ this.previousPhase = previousPhase;
+ this.nextState = nextState;
+ this.nextPlan = nextPlan;
+ this.nextPriceList = nextPriceList;
+ this.nextPhase = nextPhase;
+ this.totalOrdering = totalOrdering;
+ this.transitionType = transitionType;
+ this.remainingEventsForUserOperation = remainingEventsForUserOperation;
+ this.startDate = startDate;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.SUBSCRIPTION_TRANSITION;
+ }
+
+ @JsonProperty("eventId")
+ @Override
+ public UUID getId() {
+ return eventId;
+ }
+
+ @Override
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public EntitlementState getPreviousState() {
+ return previousState;
+ }
+
+ @Override
+ public String getPreviousPlan() {
+ return previousPlan;
+ }
+
+ @Override
+ public String getPreviousPhase() {
+ return previousPhase;
+ }
+
+ @Override
+ public String getNextPlan() {
+ return nextPlan;
+ }
+
+ @Override
+ public String getNextPhase() {
+ return nextPhase;
+ }
+
+ @Override
+ public EntitlementState getNextState() {
+ return nextState;
+ }
+
+ @Override
+ public String getPreviousPriceList() {
+ return previousPriceList;
+ }
+
+ @Override
+ public String getNextPriceList() {
+ return nextPriceList;
+ }
+
+ @Override
+ public Integer getRemainingEventsForUserOperation() {
+ return remainingEventsForUserOperation;
+ }
+
+ @Override
+ public DateTime getRequestedTransitionTime() {
+ return requestedTransitionTime;
+ }
+
+ @Override
+ public DateTime getEffectiveTransitionTime() {
+ return effectiveTransitionTime;
+ }
+
+ @Override
+ public Long getTotalOrdering() {
+ return totalOrdering;
+ }
+
+ @Override
+ public SubscriptionBaseTransitionType getTransitionType() {
+ return transitionType;
+ }
+
+ @JsonProperty("startDate")
+ @Override
+ public DateTime getSubscriptionStartDate() {
+ return startDate;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName());
+ sb.append("{bundleId=").append(bundleId);
+ sb.append(", totalOrdering=").append(totalOrdering);
+ sb.append(", subscriptionId=").append(subscriptionId);
+ sb.append(", eventId=").append(eventId);
+ sb.append(", requestedTransitionTime=").append(requestedTransitionTime);
+ sb.append(", effectiveTransitionTime=").append(effectiveTransitionTime);
+ sb.append(", previousState=").append(previousState);
+ sb.append(", previousPriceList='").append(previousPriceList).append('\'');
+ sb.append(", previousPlan='").append(previousPlan).append('\'');
+ sb.append(", previousPhase='").append(previousPhase).append('\'');
+ sb.append(", nextState=").append(nextState);
+ sb.append(", nextPriceList='").append(nextPriceList).append('\'');
+ sb.append(", nextPlan='").append(nextPlan).append('\'');
+ sb.append(", nextPhase='").append(nextPhase).append('\'');
+ sb.append(", remainingEventsForUserOperation=").append(remainingEventsForUserOperation);
+ sb.append(", transitionType=").append(transitionType);
+ sb.append(", startDate=").append(startDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultSubscriptionEvent that = (DefaultSubscriptionEvent) o;
+
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (effectiveTransitionTime != null ? effectiveTransitionTime.compareTo(that.effectiveTransitionTime) != 0 : that.effectiveTransitionTime != null) {
+ return false;
+ }
+ if (eventId != null ? !eventId.equals(that.eventId) : that.eventId != null) {
+ return false;
+ }
+ if (nextPhase != null ? !nextPhase.equals(that.nextPhase) : that.nextPhase != null) {
+ return false;
+ }
+ if (nextPlan != null ? !nextPlan.equals(that.nextPlan) : that.nextPlan != null) {
+ return false;
+ }
+ if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
+ return false;
+ }
+ if (nextState != that.nextState) {
+ return false;
+ }
+ if (previousPhase != null ? !previousPhase.equals(that.previousPhase) : that.previousPhase != null) {
+ return false;
+ }
+ if (previousPlan != null ? !previousPlan.equals(that.previousPlan) : that.previousPlan != null) {
+ return false;
+ }
+ if (previousPriceList != null ? !previousPriceList.equals(that.previousPriceList) : that.previousPriceList != null) {
+ return false;
+ }
+ if (previousState != that.previousState) {
+ return false;
+ }
+ if (remainingEventsForUserOperation != null ? !remainingEventsForUserOperation.equals(that.remainingEventsForUserOperation) : that.remainingEventsForUserOperation != null) {
+ return false;
+ }
+ if (requestedTransitionTime != null ? requestedTransitionTime.compareTo(that.requestedTransitionTime) != 0 : that.requestedTransitionTime != null) {
+ return false;
+ }
+ if (startDate != null ? startDate.compareTo(that.startDate) != 0 : that.startDate != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+ if (totalOrdering != null ? !totalOrdering.equals(that.totalOrdering) : that.totalOrdering != null) {
+ return false;
+ }
+ if (transitionType != that.transitionType) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = totalOrdering != null ? totalOrdering.hashCode() : 0;
+ result = 31 * result + (subscriptionId != null ? subscriptionId.hashCode() : 0);
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (eventId != null ? eventId.hashCode() : 0);
+ result = 31 * result + (requestedTransitionTime != null ? requestedTransitionTime.hashCode() : 0);
+ result = 31 * result + (effectiveTransitionTime != null ? effectiveTransitionTime.hashCode() : 0);
+ result = 31 * result + (previousState != null ? previousState.hashCode() : 0);
+ result = 31 * result + (previousPriceList != null ? previousPriceList.hashCode() : 0);
+ result = 31 * result + (previousPlan != null ? previousPlan.hashCode() : 0);
+ result = 31 * result + (previousPhase != null ? previousPhase.hashCode() : 0);
+ result = 31 * result + (nextState != null ? nextState.hashCode() : 0);
+ result = 31 * result + (nextPriceList != null ? nextPriceList.hashCode() : 0);
+ result = 31 * result + (nextPlan != null ? nextPlan.hashCode() : 0);
+ result = 31 * result + (nextPhase != null ? nextPhase.hashCode() : 0);
+ result = 31 * result + (remainingEventsForUserOperation != null ? remainingEventsForUserOperation.hashCode() : 0);
+ result = 31 * result + (transitionType != null ? transitionType.hashCode() : 0);
+ result = 31 * result + (startDate != null ? startDate.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionStatusDryRun.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionStatusDryRun.java
new file mode 100644
index 0000000..e530196
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionStatusDryRun.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
+
+public class DefaultSubscriptionStatusDryRun implements EntitlementAOStatusDryRun {
+
+ private final UUID id;
+ private final String productName;
+ private final PhaseType phaseType;
+ private final BillingPeriod billingPeriod;
+ private final String priceList;
+ private final DryRunChangeReason reason;
+
+
+ public DefaultSubscriptionStatusDryRun(final UUID id, final String productName,
+ final PhaseType phaseType, final BillingPeriod billingPeriod, final String priceList,
+ final DryRunChangeReason reason) {
+ super();
+ this.id = id;
+ this.productName = productName;
+ this.phaseType = phaseType;
+ this.billingPeriod = billingPeriod;
+ this.priceList = priceList;
+ this.reason = reason;
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public String getProductName() {
+ return productName;
+ }
+
+ @Override
+ public PhaseType getPhaseType() {
+ return phaseType;
+ }
+
+
+ @Override
+ public BillingPeriod getBillingPeriod() {
+ return billingPeriod;
+ }
+
+ @Override
+ public String getPriceList() {
+ return priceList;
+ }
+
+ @Override
+ public DryRunChangeReason getReason() {
+ return reason;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java
new file mode 100644
index 0000000..ab71855
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionData.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+
+public class SubscriptionBaseTransitionData implements SubscriptionBaseTransition {
+ private final Long totalOrdering;
+ private final UUID subscriptionId;
+ private final UUID bundleId;
+ private final UUID eventId;
+ private final EventType eventType;
+ private final ApiEventType apiEventType;
+ private final DateTime requestedTransitionTime;
+ private final DateTime effectiveTransitionTime;
+ private final EntitlementState previousState;
+ private final PriceList previousPriceList;
+ private final UUID previousEventId;
+ private final DateTime previousEventCreatedDate;
+ private final Plan previousPlan;
+ private final PlanPhase previousPhase;
+ private final UUID nextEventId;
+ private final DateTime nextEventCreatedDate;
+ private final EntitlementState nextState;
+ private final PriceList nextPriceList;
+ private final Plan nextPlan;
+ private final PlanPhase nextPhase;
+ private final Boolean isFromDisk;
+ private final Integer remainingEventsForUserOperation;
+ private final UUID userToken;
+ private final DateTime createdDate;
+
+ public SubscriptionBaseTransitionData(final UUID eventId,
+ final UUID subscriptionId,
+ final UUID bundleId,
+ final EventType eventType,
+ final ApiEventType apiEventType,
+ final DateTime requestedTransitionTime,
+ final DateTime effectiveTransitionTime,
+ final UUID previousEventId,
+ final DateTime previousEventCreatedDate,
+ final EntitlementState previousState,
+ final Plan previousPlan,
+ final PlanPhase previousPhase,
+ final PriceList previousPriceList,
+ final UUID nextEventId,
+ final DateTime nextEventCreatedDate,
+ final EntitlementState nextState,
+ final Plan nextPlan,
+ final PlanPhase nextPhase,
+ final PriceList nextPriceList,
+ final Long totalOrdering,
+ final DateTime createdDate,
+ final UUID userToken,
+ final Boolean isFromDisk) {
+ this.eventId = eventId;
+ this.subscriptionId = subscriptionId;
+ this.bundleId = bundleId;
+ this.eventType = eventType;
+ this.apiEventType = apiEventType;
+ this.requestedTransitionTime = requestedTransitionTime;
+ this.effectiveTransitionTime = effectiveTransitionTime;
+ this.previousState = previousState;
+ this.previousPriceList = previousPriceList;
+ this.previousPlan = previousPlan;
+ this.previousPhase = previousPhase;
+ this.nextState = nextState;
+ this.nextPlan = nextPlan;
+ this.nextPriceList = nextPriceList;
+ this.nextPhase = nextPhase;
+ this.totalOrdering = totalOrdering;
+ this.previousEventId = previousEventId;
+ this.previousEventCreatedDate = previousEventCreatedDate;
+ this.nextEventId = nextEventId;
+ this.nextEventCreatedDate = nextEventCreatedDate;
+ this.isFromDisk = isFromDisk;
+ this.userToken = userToken;
+ this.createdDate = createdDate;
+ this.remainingEventsForUserOperation = 0;
+ }
+
+ public SubscriptionBaseTransitionData(final SubscriptionBaseTransitionData input, int remainingEventsForUserOperation) {
+ this(input, input.getEventType(), input.getApiEventType(), remainingEventsForUserOperation);
+ }
+
+ public SubscriptionBaseTransitionData(final SubscriptionBaseTransitionData input, final EventType eventType,
+ final ApiEventType apiEventType, int remainingEventsForUserOperation) {
+ super();
+ this.eventId = input.getId();
+ this.subscriptionId = input.getSubscriptionId();
+ this.bundleId = input.getBundleId();
+ this.eventType = eventType;
+ this.apiEventType = apiEventType;
+ this.requestedTransitionTime = input.getRequestedTransitionTime();
+ this.effectiveTransitionTime = input.getEffectiveTransitionTime();
+ this.previousEventId = input.getPreviousEventId();
+ this.previousEventCreatedDate = input.getPreviousEventCreatedDate();
+ this.previousState = input.getPreviousState();
+ this.previousPriceList = input.getPreviousPriceList();
+ this.previousPlan = input.getPreviousPlan();
+ this.previousPhase = input.getPreviousPhase();
+ this.nextEventId = input.getNextEventId();
+ this.nextEventCreatedDate = input.getNextEventCreatedDate();
+ this.nextState = input.getNextState();
+ this.nextPlan = input.getNextPlan();
+ this.nextPriceList = input.getNextPriceList();
+ this.nextPhase = input.getNextPhase();
+ this.totalOrdering = input.getTotalOrdering();
+ this.isFromDisk = input.isFromDisk();
+ this.userToken = input.getUserToken();
+ this.remainingEventsForUserOperation = remainingEventsForUserOperation;
+ this.createdDate = input.getCreatedDate();
+ }
+
+ @Override
+ public UUID getId() {
+ return eventId;
+ }
+
+ @Override
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public EntitlementState getPreviousState() {
+ return previousState;
+ }
+
+ @Override
+ public Plan getPreviousPlan() {
+ return previousPlan;
+ }
+
+ @Override
+ public PlanPhase getPreviousPhase() {
+ return previousPhase;
+ }
+
+ @Override
+ public UUID getNextEventId() {
+ return nextEventId;
+ }
+
+ @Override
+ public DateTime getNextEventCreatedDate() {
+ return nextEventCreatedDate;
+ }
+
+ @Override
+ public Plan getNextPlan() {
+ return nextPlan;
+ }
+
+ @Override
+ public PlanPhase getNextPhase() {
+ return nextPhase;
+ }
+
+ @Override
+ public EntitlementState getNextState() {
+ return nextState;
+ }
+
+ @Override
+ public UUID getPreviousEventId() {
+ return previousEventId;
+ }
+
+ @Override
+ public DateTime getPreviousEventCreatedDate() {
+ return previousEventCreatedDate;
+ }
+
+ @Override
+ public PriceList getPreviousPriceList() {
+ return previousPriceList;
+ }
+
+ @Override
+ public PriceList getNextPriceList() {
+ return nextPriceList;
+ }
+
+ public UUID getUserToken() {
+ return userToken;
+ }
+
+ public Integer getRemainingEventsForUserOperation() {
+ return remainingEventsForUserOperation;
+ }
+
+ @Override
+ public SubscriptionBaseTransitionType getTransitionType() {
+ return toSubscriptionTransitionType(eventType, apiEventType);
+ }
+
+ public static SubscriptionBaseTransitionType toSubscriptionTransitionType(final EventType eventType, final ApiEventType apiEventType) {
+ switch (eventType) {
+ case API_USER:
+ return apiEventType.getSubscriptionTransitionType();
+ case PHASE:
+ return SubscriptionBaseTransitionType.PHASE;
+ default:
+ throw new SubscriptionBaseError("Unexpected event type " + eventType);
+ }
+ }
+ @Override
+ public DateTime getCreatedDate() {
+ return createdDate;
+ }
+
+ @Override
+ public DateTime getRequestedTransitionTime() {
+ return requestedTransitionTime;
+ }
+
+ @Override
+ public DateTime getEffectiveTransitionTime() {
+ return effectiveTransitionTime;
+ }
+
+ public Long getTotalOrdering() {
+ return totalOrdering;
+ }
+
+ public Boolean isFromDisk() {
+ return isFromDisk;
+ }
+
+ public ApiEventType getApiEventType() {
+ return apiEventType;
+ }
+
+ public EventType getEventType() {
+ return eventType;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SubscriptionBaseTransitionData");
+ sb.append("{apiEventType=").append(apiEventType);
+ sb.append(", totalOrdering=").append(totalOrdering);
+ sb.append(", subscriptionId=").append(subscriptionId);
+ sb.append(", bundleId=").append(bundleId);
+ sb.append(", eventId=").append(eventId);
+ sb.append(", eventType=").append(eventType);
+ sb.append(", requestedTransitionTime=").append(requestedTransitionTime);
+ sb.append(", effectiveTransitionTime=").append(effectiveTransitionTime);
+ sb.append(", previousState=").append(previousState);
+ sb.append(", previousPriceList=").append(previousPriceList);
+ sb.append(", previousPlan=").append(previousPlan);
+ sb.append(", previousPhase=").append(previousPhase);
+ sb.append(", nextState=").append(nextState);
+ sb.append(", nextPriceList=").append(nextPriceList);
+ sb.append(", nextPlan=").append(nextPlan);
+ sb.append(", nextPhase=").append(nextPhase);
+ sb.append(", isFromDisk=").append(isFromDisk);
+ sb.append(", remainingEventsForUserOperation=").append(remainingEventsForUserOperation);
+ sb.append(", userToken=").append(userToken);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final SubscriptionBaseTransitionData that = (SubscriptionBaseTransitionData) o;
+
+ if (apiEventType != that.apiEventType) {
+ return false;
+ }
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (effectiveTransitionTime != null ? effectiveTransitionTime.compareTo(that.effectiveTransitionTime) != 0 : that.effectiveTransitionTime != null) {
+ return false;
+ }
+ if (eventId != null ? !eventId.equals(that.eventId) : that.eventId != null) {
+ return false;
+ }
+ if (eventType != that.eventType) {
+ return false;
+ }
+ if (isFromDisk != null ? !isFromDisk.equals(that.isFromDisk) : that.isFromDisk != null) {
+ return false;
+ }
+ if (nextPhase != null ? !nextPhase.equals(that.nextPhase) : that.nextPhase != null) {
+ return false;
+ }
+ if (nextPlan != null ? !nextPlan.equals(that.nextPlan) : that.nextPlan != null) {
+ return false;
+ }
+ if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
+ return false;
+ }
+ if (nextState != that.nextState) {
+ return false;
+ }
+ if (previousPhase != null ? !previousPhase.equals(that.previousPhase) : that.previousPhase != null) {
+ return false;
+ }
+ if (previousPlan != null ? !previousPlan.equals(that.previousPlan) : that.previousPlan != null) {
+ return false;
+ }
+ if (previousPriceList != null ? !previousPriceList.equals(that.previousPriceList) : that.previousPriceList != null) {
+ return false;
+ }
+ if (previousState != that.previousState) {
+ return false;
+ }
+ if (remainingEventsForUserOperation != null ? !remainingEventsForUserOperation.equals(that.remainingEventsForUserOperation) : that.remainingEventsForUserOperation != null) {
+ return false;
+ }
+ if (requestedTransitionTime != null ? requestedTransitionTime.compareTo(that.requestedTransitionTime) != 0 : that.requestedTransitionTime != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+ if (totalOrdering != null ? !totalOrdering.equals(that.totalOrdering) : that.totalOrdering != null) {
+ return false;
+ }
+ if (userToken != null ? !userToken.equals(that.userToken) : that.userToken != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = totalOrdering != null ? totalOrdering.hashCode() : 0;
+ result = 31 * result + (subscriptionId != null ? subscriptionId.hashCode() : 0);
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (eventId != null ? eventId.hashCode() : 0);
+ result = 31 * result + (eventType != null ? eventType.hashCode() : 0);
+ result = 31 * result + (apiEventType != null ? apiEventType.hashCode() : 0);
+ result = 31 * result + (requestedTransitionTime != null ? requestedTransitionTime.hashCode() : 0);
+ result = 31 * result + (effectiveTransitionTime != null ? effectiveTransitionTime.hashCode() : 0);
+ result = 31 * result + (previousState != null ? previousState.hashCode() : 0);
+ result = 31 * result + (previousPriceList != null ? previousPriceList.hashCode() : 0);
+ result = 31 * result + (previousPlan != null ? previousPlan.hashCode() : 0);
+ result = 31 * result + (previousPhase != null ? previousPhase.hashCode() : 0);
+ result = 31 * result + (nextState != null ? nextState.hashCode() : 0);
+ result = 31 * result + (nextPriceList != null ? nextPriceList.hashCode() : 0);
+ result = 31 * result + (nextPlan != null ? nextPlan.hashCode() : 0);
+ result = 31 * result + (nextPhase != null ? nextPhase.hashCode() : 0);
+ result = 31 * result + (isFromDisk != null ? isFromDisk.hashCode() : 0);
+ result = 31 * result + (remainingEventsForUserOperation != null ? remainingEventsForUserOperation.hashCode() : 0);
+ result = 31 * result + (userToken != null ? userToken.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionDataIterator.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionDataIterator.java
new file mode 100644
index 0000000..881e24f
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBaseTransitionDataIterator.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.clock.Clock;
+
+public class SubscriptionBaseTransitionDataIterator implements Iterator<SubscriptionBaseTransition> {
+
+ private final Clock clock;
+ private final Iterator<SubscriptionBaseTransition> it;
+ private final Kind kind;
+ private final TimeLimit timeLimit;
+ private final Visibility visibility;
+
+ private SubscriptionBaseTransition next;
+
+ public enum Order {
+ ASC_FROM_PAST,
+ DESC_FROM_FUTURE
+ }
+
+ public enum Kind {
+ SUBSCRIPTION,
+ BILLING,
+ ALL
+ }
+
+ public enum TimeLimit {
+ FUTURE_ONLY,
+ PAST_OR_PRESENT_ONLY,
+ ALL
+ }
+
+ public enum Visibility {
+ FROM_DISK_ONLY,
+ ALL
+ }
+
+ public SubscriptionBaseTransitionDataIterator(final Clock clock, final LinkedList<SubscriptionBaseTransition> transitions,
+ final Order order, final Kind kind, final Visibility visibility, final TimeLimit timeLimit) {
+ this.it = (order == Order.DESC_FROM_FUTURE) ? transitions.descendingIterator() : transitions.iterator();
+ this.clock = clock;
+ this.kind = kind;
+ this.timeLimit = timeLimit;
+ this.visibility = visibility;
+ this.next = null;
+ }
+
+ @Override
+ public boolean hasNext() {
+ do {
+ final boolean hasNext = it.hasNext();
+ if (!hasNext) {
+ return false;
+ }
+ next = it.next();
+ } while (shouldSkip(next));
+ return true;
+ }
+
+ private boolean shouldSkip(final SubscriptionBaseTransition input) {
+ if (visibility == Visibility.FROM_DISK_ONLY && ! ((SubscriptionBaseTransitionData) input).isFromDisk()) {
+ return true;
+ }
+ if ((kind == Kind.SUBSCRIPTION && shouldSkipForSubscriptionEvents((SubscriptionBaseTransitionData) input)) ||
+ (kind == Kind.BILLING && shouldSkipForBillingEvents((SubscriptionBaseTransitionData) input))) {
+ return true;
+ }
+ if ((timeLimit == TimeLimit.FUTURE_ONLY && !input.getEffectiveTransitionTime().isAfter(clock.getUTCNow())) ||
+ ((timeLimit == TimeLimit.PAST_OR_PRESENT_ONLY && input.getEffectiveTransitionTime().isAfter(clock.getUTCNow())))) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean shouldSkipForSubscriptionEvents(final SubscriptionBaseTransitionData input) {
+ // SubscriptionBase system knows about all events except for MIGRATE_BILLING
+ return (input.getTransitionType() == SubscriptionBaseTransitionType.MIGRATE_BILLING);
+ }
+
+ private boolean shouldSkipForBillingEvents(final SubscriptionBaseTransitionData input) {
+ // Junction system knows about all events except for MIGRATE_ENTITLEMENT
+ return input.getTransitionType() == SubscriptionBaseTransitionType.MIGRATE_ENTITLEMENT;
+ }
+
+
+ @Override
+ public SubscriptionBaseTransition next() {
+ return next;
+ }
+
+ @Override
+ public void remove() {
+ throw new SubscriptionBaseError("Operation SubscriptionBaseTransitionDataIterator.remove not implemented");
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBuilder.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBuilder.java
new file mode 100644
index 0000000..a23502f
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionBuilder.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.lang.reflect.Field;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+
+public class SubscriptionBuilder {
+
+ private UUID id;
+ private UUID bundleId;
+ private DateTime createdDate;
+ private DateTime updatedDate;
+ private DateTime alignStartDate;
+ private DateTime bundleStartDate;
+ private Long activeVersion;
+ private ProductCategory category;
+ private DateTime chargedThroughDate;
+
+ public SubscriptionBuilder() {
+ this.activeVersion = SubscriptionEvents.INITIAL_VERSION;
+ }
+
+ public SubscriptionBuilder(final DefaultSubscriptionBase original) {
+ this.id = original.getId();
+ this.bundleId = original.getBundleId();
+ this.alignStartDate = original.getAlignStartDate();
+ this.bundleStartDate = original.getBundleStartDate();
+ this.category = original.getCategory();
+ this.activeVersion = original.getActiveVersion();
+ this.chargedThroughDate = original.getChargedThroughDate();
+ }
+
+ public SubscriptionBuilder setId(final UUID id) {
+ this.id = id;
+ return this;
+ }
+
+ public SubscriptionBuilder setCreatedDate(final DateTime createdDate) {
+ this.createdDate = createdDate;
+ return this;
+ }
+
+ public SubscriptionBuilder setUpdatedDate(final DateTime updatedDate) {
+ this.updatedDate = updatedDate;
+ return this;
+ }
+
+ public SubscriptionBuilder setBundleId(final UUID bundleId) {
+ this.bundleId = bundleId;
+ return this;
+ }
+
+ public SubscriptionBuilder setAlignStartDate(final DateTime alignStartDate) {
+ this.alignStartDate = alignStartDate;
+ return this;
+ }
+
+ public SubscriptionBuilder setBundleStartDate(final DateTime bundleStartDate) {
+ this.bundleStartDate = bundleStartDate;
+ return this;
+ }
+
+ public SubscriptionBuilder setActiveVersion(final long activeVersion) {
+ this.activeVersion = activeVersion;
+ return this;
+ }
+
+ public SubscriptionBuilder setChargedThroughDate(final DateTime chargedThroughDate) {
+ this.chargedThroughDate = chargedThroughDate;
+ return this;
+ }
+
+ public SubscriptionBuilder setCategory(final ProductCategory category) {
+ this.category = category;
+ return this;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public DateTime getCreatedDate() {
+ return createdDate;
+ }
+
+ public DateTime getUpdatedDate() {
+ return updatedDate;
+ }
+
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ public DateTime getAlignStartDate() {
+ return alignStartDate;
+ }
+
+ public DateTime getBundleStartDate() {
+ return bundleStartDate;
+ }
+
+ public Long getActiveVersion() {
+ return activeVersion;
+ }
+
+ public ProductCategory getCategory() {
+ return category;
+ }
+
+ public DateTime getChargedThroughDate() {
+ return chargedThroughDate;
+ }
+
+ private void checkAllFieldsSet() {
+ for (final Field cur : SubscriptionBuilder.class.getDeclaredFields()) {
+ try {
+ final Object value = cur.get(this);
+ if (value == null) {
+ throw new SubscriptionBaseError(String.format("Field %s has not been set for SubscriptionBase",
+ cur.getName()));
+ }
+ } catch (IllegalAccessException e) {
+ throw new SubscriptionBaseError(String.format("Failed to access value for field %s for SubscriptionBase",
+ cur.getName()), e);
+ }
+ }
+ }
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionEvents.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionEvents.java
new file mode 100644
index 0000000..46bc931
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionEvents.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+
+
+public class SubscriptionEvents {
+ public static final long INITIAL_VERSION = 1;
+
+ private final List<SubscriptionBaseEvent> events;
+
+ private long activeVersion;
+
+ public SubscriptionEvents() {
+ this.events = new LinkedList<SubscriptionBaseEvent>();
+ this.activeVersion = INITIAL_VERSION;
+ }
+
+ public void addEvent(final SubscriptionBaseEvent ev) {
+ events.add(ev);
+ }
+
+ public List<SubscriptionBaseEvent> getCurrentView() {
+ return getViewForVersion(activeVersion);
+ }
+
+ public List<SubscriptionBaseEvent> getViewForVersion(final long version) {
+ final LinkedList<SubscriptionBaseEvent> result = new LinkedList<SubscriptionBaseEvent>();
+ for (final SubscriptionBaseEvent cur : events) {
+ if (cur.getActiveVersion() == version) {
+ result.add(cur);
+ }
+ }
+
+ return result;
+ }
+
+ public long getActiveVersion() {
+ return activeVersion;
+ }
+
+ public void setActiveVersion(final long activeVersion) {
+ this.activeVersion = activeVersion;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SubscriptionEvents");
+ sb.append("{activeVersion=").append(activeVersion);
+ sb.append(", events=").append(events);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final SubscriptionEvents that = (SubscriptionEvents) o;
+
+ if (activeVersion != that.activeVersion) {
+ return false;
+ }
+ if (events != null ? !events.equals(that.events) : that.events != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = events != null ? events.hashCode() : 0;
+ result = 31 * result + (int) (activeVersion ^ (activeVersion >>> 32));
+ return result;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java
new file mode 100644
index 0000000..83dcac1
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.addon;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+
+import com.google.inject.Inject;
+
+public class AddonUtils {
+ private final CatalogService catalogService;
+
+ @Inject
+ public AddonUtils(final CatalogService catalogService) {
+ this.catalogService = catalogService;
+ }
+
+ public void checkAddonCreationRights(final DefaultSubscriptionBase baseSubscription, final Plan targetAddOnPlan)
+ throws SubscriptionBaseApiException, CatalogApiException {
+
+ if (baseSubscription.getState() != EntitlementState.ACTIVE) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_BP_NON_ACTIVE, targetAddOnPlan.getName());
+ }
+
+ final Product baseProduct = baseSubscription.getCurrentPlan().getProduct();
+ if (isAddonIncluded(baseProduct, targetAddOnPlan)) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_ALREADY_INCLUDED,
+ targetAddOnPlan.getName(), baseSubscription.getCurrentPlan().getProduct().getName());
+ }
+
+ if (!isAddonAvailable(baseProduct, targetAddOnPlan)) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_NOT_AVAILABLE,
+ targetAddOnPlan.getName(), baseSubscription.getCurrentPlan().getProduct().getName());
+ }
+ }
+
+ public boolean isAddonAvailableFromProdName(final String baseProductName, final DateTime requestedDate, final Plan targetAddOnPlan) {
+ try {
+ final Product product = catalogService.getFullCatalog().findProduct(baseProductName, requestedDate);
+ return isAddonAvailable(product, targetAddOnPlan);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseError(e);
+ }
+ }
+
+ public boolean isAddonAvailableFromPlanName(final String basePlanName, final DateTime requestedDate, final Plan targetAddOnPlan) {
+ try {
+ final Plan plan = catalogService.getFullCatalog().findPlan(basePlanName, requestedDate);
+ final Product product = plan.getProduct();
+ return isAddonAvailable(product, targetAddOnPlan);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseError(e);
+ }
+ }
+
+ public boolean isAddonAvailable(final Product baseProduct, final Plan targetAddOnPlan) {
+ final Product targetAddonProduct = targetAddOnPlan.getProduct();
+ final Product[] availableAddOns = baseProduct.getAvailable();
+
+ for (final Product curAv : availableAddOns) {
+ if (curAv.getName().equals(targetAddonProduct.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isAddonIncludedFromProdName(final String baseProductName, final DateTime requestedDate, final Plan targetAddOnPlan) {
+ try {
+ final Product product = catalogService.getFullCatalog().findProduct(baseProductName, requestedDate);
+ return isAddonIncluded(product, targetAddOnPlan);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseError(e);
+ }
+
+ }
+
+ public boolean isAddonIncludedFromPlanName(final String basePlanName, final DateTime requestedDate, final Plan targetAddOnPlan) {
+ try {
+ final Plan plan = catalogService.getFullCatalog().findPlan(basePlanName, requestedDate);
+ final Product product = plan.getProduct();
+ return isAddonIncluded(product, targetAddOnPlan);
+ } catch (CatalogApiException e) {
+ throw new SubscriptionBaseError(e);
+ }
+ }
+
+ public boolean isAddonIncluded(final Product baseProduct, final Plan targetAddOnPlan) {
+ final Product targetAddonProduct = targetAddOnPlan.getProduct();
+ final Product[] includedAddOns = baseProduct.getIncluded();
+ for (final Product curAv : includedAddOns) {
+ if (curAv.getName().equals(targetAddonProduct.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
new file mode 100644
index 0000000..8447d80
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/DefaultSubscriptionBaseService.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.core;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.bus.api.PersistentBus.EventBusException;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.subscription.alignment.PlanAligner;
+import org.killbill.billing.subscription.alignment.TimedPhase;
+import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.SubscriptionBaseService;
+import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
+import org.killbill.billing.subscription.engine.addon.AddonUtils;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.phase.PhaseEvent;
+import org.killbill.billing.subscription.events.phase.PhaseEventData;
+import org.killbill.billing.subscription.events.user.ApiEvent;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.lifecycle.LifecycleHandlerType;
+import org.killbill.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists;
+import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueHandler;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.clock.Clock;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+
+import com.google.inject.Inject;
+
+public class DefaultSubscriptionBaseService implements EventListener, SubscriptionBaseService {
+
+ public static final String NOTIFICATION_QUEUE_NAME = "subscription-events";
+ public static final String SUBSCRIPTION_SERVICE_NAME = "subscription-service";
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultSubscriptionBaseService.class);
+
+ private final Clock clock;
+ private final SubscriptionDao dao;
+ private final PlanAligner planAligner;
+ private final AddonUtils addonUtils;
+ private final PersistentBus eventBus;
+ private final NotificationQueueService notificationQueueService;
+ private final InternalCallContextFactory internalCallContextFactory;
+ private NotificationQueue subscriptionEventQueue;
+ private final SubscriptionBaseApiService apiService;
+
+ @Inject
+ public DefaultSubscriptionBaseService(final Clock clock, final SubscriptionDao dao, final PlanAligner planAligner,
+ final AddonUtils addonUtils, final PersistentBus eventBus,
+ final NotificationQueueService notificationQueueService,
+ final InternalCallContextFactory internalCallContextFactory,
+ final SubscriptionBaseApiService apiService) {
+ this.clock = clock;
+ this.dao = dao;
+ this.planAligner = planAligner;
+ this.addonUtils = addonUtils;
+ this.eventBus = eventBus;
+ this.notificationQueueService = notificationQueueService;
+ this.internalCallContextFactory = internalCallContextFactory;
+ this.apiService = apiService;
+ }
+
+ @Override
+ public String getName() {
+ return SUBSCRIPTION_SERVICE_NAME;
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.INIT_SERVICE)
+ public void initialize() {
+ try {
+ final NotificationQueueHandler queueHandler = new NotificationQueueHandler() {
+ @Override
+ public void handleReadyNotification(final NotificationEvent inputKey, final DateTime eventDateTime, final UUID fromNotificationQueueUserToken, final Long accountRecordId, final Long tenantRecordId) {
+ if (!(inputKey instanceof SubscriptionNotificationKey)) {
+ log.error("SubscriptionBase service received an unexpected event type {}" + inputKey.getClass().getName());
+ return;
+ }
+
+ final SubscriptionNotificationKey key = (SubscriptionNotificationKey) inputKey;
+ final SubscriptionBaseEvent event = dao.getEventById(key.getEventId(), internalCallContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId));
+ if (event == null) {
+ // This can be expected if the event is soft deleted (is_active = 0)
+ log.info("Failed to extract event for notification key {}", inputKey);
+ return;
+ }
+
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "SubscriptionEventQueue", CallOrigin.INTERNAL, UserType.SYSTEM, fromNotificationQueueUserToken);
+ processEventReady(event, key.getSeqId(), context);
+ }
+ };
+
+ subscriptionEventQueue = notificationQueueService.createNotificationQueue(SUBSCRIPTION_SERVICE_NAME,
+ NOTIFICATION_QUEUE_NAME,
+ queueHandler);
+ } catch (NotificationQueueAlreadyExists e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.START_SERVICE)
+ public void start() {
+ subscriptionEventQueue.startQueue();
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE)
+ public void stop() throws NoSuchNotificationQueue {
+ if (subscriptionEventQueue != null) {
+ subscriptionEventQueue.stopQueue();
+ notificationQueueService.deleteNotificationQueue(subscriptionEventQueue.getServiceName(), subscriptionEventQueue.getQueueName());
+ }
+ }
+
+ @Override
+ public void processEventReady(final SubscriptionBaseEvent event, final int seqId, final InternalCallContext context) {
+ if (!event.isActive()) {
+ return;
+ }
+
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) dao.getSubscriptionFromId(event.getSubscriptionId(), context);
+ if (subscription == null) {
+ log.warn("Failed to retrieve subscription for id %s", event.getSubscriptionId());
+ return;
+ }
+ if (subscription.getActiveVersion() > event.getActiveVersion()) {
+ // Skip repaired events
+ return;
+ }
+
+ //
+ // Do any internal processing on that event before we send the event to the bus
+ //
+ int theRealSeqId = seqId;
+ if (event.getType() == EventType.PHASE) {
+ onPhaseEvent(subscription, context);
+ } else if (event.getType() == EventType.API_USER && subscription.getCategory() == ProductCategory.BASE) {
+ theRealSeqId = onBasePlanEvent(subscription, (ApiEvent) event, context);
+ }
+
+ try {
+ final SubscriptionBaseTransitionData transition = (subscription.getTransitionFromEvent(event, theRealSeqId));
+ final EffectiveSubscriptionInternalEvent busEvent = new DefaultEffectiveSubscriptionEvent(transition, subscription.getAlignStartDate(),
+ context.getUserToken(),
+ context.getAccountRecordId(), context.getTenantRecordId());
+ eventBus.post(busEvent);
+ } catch (EventBusException e) {
+ log.warn("Failed to post subscription event " + event, e);
+ }
+ }
+
+ private void onPhaseEvent(final DefaultSubscriptionBase subscription, final InternalCallContext context) {
+ try {
+ final DateTime now = clock.getUTCNow();
+ final TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription, now, now);
+ final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+ PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+ null;
+ if (nextPhaseEvent != null) {
+ dao.createNextPhaseEvent(subscription, nextPhaseEvent, context);
+ }
+ } catch (SubscriptionBaseError e) {
+ log.error(String.format("Failed to insert next phase for subscription %s", subscription.getId()), e);
+ }
+ }
+
+ private int onBasePlanEvent(final DefaultSubscriptionBase baseSubscription, final ApiEvent event, final InternalCallContext context) {
+ return apiService.cancelAddOnsIfRequired(baseSubscription, event.getEffectiveDate(), context);
+ }
+
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/EventListener.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/EventListener.java
new file mode 100644
index 0000000..31b31b2
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/EventListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.core;
+
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.callcontext.InternalCallContext;
+
+
+public interface EventListener {
+
+ public void processEventReady(final SubscriptionBaseEvent event, final int seqId, final InternalCallContext context);
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/core/SubscriptionNotificationKey.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/SubscriptionNotificationKey.java
new file mode 100644
index 0000000..3c10e8f
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/core/SubscriptionNotificationKey.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.core;
+
+import java.util.UUID;
+
+import org.killbill.notificationq.api.NotificationEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class SubscriptionNotificationKey implements NotificationEvent {
+
+ private final UUID eventId;
+ private final int seqId;
+
+
+ @JsonCreator
+ public SubscriptionNotificationKey(@JsonProperty("eventId") final UUID eventId,
+ @JsonProperty("seqId") final int seqId) {
+ this.eventId = eventId;
+ this.seqId = seqId;
+ }
+
+ public SubscriptionNotificationKey(final UUID eventId) {
+ this(eventId, 0);
+ }
+
+ public UUID getEventId() {
+ return eventId;
+ }
+
+ public int getSeqId() {
+ return seqId;
+ }
+
+ public String toString() {
+ if (seqId == 0) {
+ return eventId.toString();
+ } else {
+ return eventId.toString() + ":" + seqId;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((eventId == null) ? 0 : eventId.hashCode());
+ result = prime * result + seqId;
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final SubscriptionNotificationKey other = (SubscriptionNotificationKey) obj;
+ if (eventId == null) {
+ if (other.eventId != null) {
+ return false;
+ }
+ } else if (!eventId.equals(other.eventId)) {
+ return false;
+ }
+ if (seqId != other.seqId) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/BundleSqlDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/BundleSqlDao.java
new file mode 100644
index 0000000..585e013
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/BundleSqlDao.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.dao;
+
+import java.util.Date;
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface BundleSqlDao extends EntitySqlDao<SubscriptionBundleModelDao, SubscriptionBaseBundle> {
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ public void updateBundleExternalKey(@Bind("id") String id,
+ @Bind("externalKey") String externalKey,
+ @BindBean final InternalCallContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ public void updateBundleLastSysTime(@Bind("id") String id,
+ @Bind("lastSysUpdateDate") Date lastSysUpdate,
+ @BindBean final InternalCallContext context);
+
+ @SqlQuery
+ public List<SubscriptionBundleModelDao> getBundlesFromAccountAndKey(@Bind("accountId") String accountId,
+ @Bind("externalKey") String externalKey,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public List<SubscriptionBundleModelDao> getBundleFromAccount(@Bind("accountId") String accountId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public List<SubscriptionBundleModelDao> getBundlesForKey(@Bind("externalKey") String externalKey,
+ @BindBean final InternalTenantContext context);
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionBundleModelDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionBundleModelDao.java
new file mode 100644
index 0000000..df7d34f
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionBundleModelDao.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.dao.model;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class SubscriptionBundleModelDao extends EntityBase implements EntityModelDao<SubscriptionBaseBundle> {
+
+ private String externalKey;
+ private UUID accountId;
+ private DateTime lastSysUpdateDate;
+ private DateTime originalCreatedDate;
+
+ public SubscriptionBundleModelDao() { /* For the DAO mapper */ }
+
+ public SubscriptionBundleModelDao(final UUID id, final String key, final UUID accountId, final DateTime lastSysUpdateDate,
+ final DateTime createdDate, DateTime originalCreatedDate, final DateTime updateDate) {
+ super(id, createdDate, updateDate);
+ this.externalKey = key;
+ this.accountId = accountId;
+ this.lastSysUpdateDate = lastSysUpdateDate;
+ this.originalCreatedDate = originalCreatedDate;
+ }
+
+ public SubscriptionBundleModelDao(final DefaultSubscriptionBaseBundle input) {
+ this(input.getId(), input.getExternalKey(), input.getAccountId(), input.getLastSysUpdateDate(), input.getCreatedDate(), input.getOriginalCreatedDate(), input.getUpdatedDate());
+ }
+
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ public DateTime getLastSysUpdateDate() {
+ return lastSysUpdateDate;
+ }
+
+ public DateTime getOriginalCreatedDate() {
+ return originalCreatedDate;
+ }
+
+ public void setExternalKey(final String externalKey) {
+ this.externalKey = externalKey;
+ }
+
+ public void setAccountId(final UUID accountId) {
+ this.accountId = accountId;
+ }
+
+ public void setLastSysUpdateDate(final DateTime lastSysUpdateDate) {
+ this.lastSysUpdateDate = lastSysUpdateDate;
+ }
+
+ public void setOriginalCreatedDate(final DateTime originalCreatedDate) {
+ this.originalCreatedDate = originalCreatedDate;
+ }
+
+ public static SubscriptionBaseBundle toSubscriptionbundle(final SubscriptionBundleModelDao src) {
+ if (src == null) {
+ return null;
+ }
+ return new DefaultSubscriptionBaseBundle(src.getId(), src.getExternalKey(), src.getAccountId(), src.getLastSysUpdateDate(), src.getOriginalCreatedDate(), src.getCreatedDate(), src.getUpdatedDate());
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SubscriptionBundleModelDao");
+ sb.append("{externalKey='").append(externalKey).append('\'');
+ sb.append(", accountId=").append(accountId);
+ sb.append(", lastSysUpdateDate=").append(lastSysUpdateDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final SubscriptionBundleModelDao that = (SubscriptionBundleModelDao) o;
+
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
+ }
+ if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
+ return false;
+ }
+ if (lastSysUpdateDate != null ? lastSysUpdateDate.compareTo(that.lastSysUpdateDate) != 0 : that.lastSysUpdateDate != null) {
+ return false;
+ }
+ if (originalCreatedDate != null ? originalCreatedDate.compareTo(that.originalCreatedDate) != 0 : that.originalCreatedDate != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+ result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+ result = 31 * result + (lastSysUpdateDate != null ? lastSysUpdateDate.hashCode() : 0);
+ result = 31 * result + (originalCreatedDate != null ? originalCreatedDate.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.BUNDLES;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return null;
+ }
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
new file mode 100644
index 0000000..dde70c9
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionEventModelDao.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.dao.model;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.EventBaseBuilder;
+import org.killbill.billing.subscription.events.phase.PhaseEvent;
+import org.killbill.billing.subscription.events.phase.PhaseEventBuilder;
+import org.killbill.billing.subscription.events.phase.PhaseEventData;
+import org.killbill.billing.subscription.events.user.ApiEvent;
+import org.killbill.billing.subscription.events.user.ApiEventBuilder;
+import org.killbill.billing.subscription.events.user.ApiEventCancel;
+import org.killbill.billing.subscription.events.user.ApiEventChange;
+import org.killbill.billing.subscription.events.user.ApiEventCreate;
+import org.killbill.billing.subscription.events.user.ApiEventMigrateBilling;
+import org.killbill.billing.subscription.events.user.ApiEventMigrateSubscription;
+import org.killbill.billing.subscription.events.user.ApiEventReCreate;
+import org.killbill.billing.subscription.events.user.ApiEventTransfer;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+import org.killbill.billing.subscription.events.user.ApiEventUncancel;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class SubscriptionEventModelDao extends EntityBase implements EntityModelDao<SubscriptionBaseEvent> {
+
+ private long totalOrdering;
+ private EventType eventType;
+ private ApiEventType userType;
+ private DateTime requestedDate;
+ private DateTime effectiveDate;
+ private UUID subscriptionId;
+ private String planName;
+ private String phaseName;
+ private String priceListName;
+ private long currentVersion;
+ private boolean isActive;
+
+ public SubscriptionEventModelDao() {
+ /* For the DAO mapper */
+ }
+
+ public SubscriptionEventModelDao(final UUID id, final long totalOrdering, final EventType eventType, final ApiEventType userType,
+ final DateTime requestedDate, final DateTime effectiveDate, final UUID subscriptionId,
+ final String planName, final String phaseName, final String priceListName, final long currentVersion,
+ final boolean active, final DateTime createDate, final DateTime updateDate) {
+ super(id, createDate, updateDate);
+ this.totalOrdering = totalOrdering;
+ this.eventType = eventType;
+ this.userType = userType;
+ this.requestedDate = requestedDate;
+ this.effectiveDate = effectiveDate;
+ this.subscriptionId = subscriptionId;
+ this.planName = planName;
+ this.phaseName = phaseName;
+ this.priceListName = priceListName;
+ this.currentVersion = currentVersion;
+ this.isActive = active;
+ }
+
+ public SubscriptionEventModelDao(final SubscriptionBaseEvent src) {
+ super(src.getId(), src.getCreatedDate(), src.getUpdatedDate());
+ this.totalOrdering = src.getTotalOrdering();
+ this.eventType = src.getType();
+ this.userType = eventType == EventType.API_USER ? ((ApiEvent) src).getEventType() : null;
+ this.requestedDate = src.getRequestedDate();
+ this.effectiveDate = src.getEffectiveDate();
+ this.subscriptionId = src.getSubscriptionId();
+ this.planName = eventType == EventType.API_USER ? ((ApiEvent) src).getEventPlan() : null;
+ this.phaseName = eventType == EventType.API_USER ? ((ApiEvent) src).getEventPlanPhase() : ((PhaseEvent) src).getPhase();
+ this.priceListName = eventType == EventType.API_USER ? ((ApiEvent) src).getPriceList() : null;
+ this.currentVersion = src.getActiveVersion();
+ this.isActive = src.isActive();
+ }
+
+ public long getTotalOrdering() {
+ return totalOrdering;
+ }
+
+ public EventType getEventType() {
+ return eventType;
+ }
+
+ public ApiEventType getUserType() {
+ return userType;
+ }
+
+ public DateTime getRequestedDate() {
+ return requestedDate;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ public String getPlanName() {
+ return planName;
+ }
+
+ public String getPhaseName() {
+ return phaseName;
+ }
+
+ public String getPriceListName() {
+ return priceListName;
+ }
+
+ public long getCurrentVersion() {
+ return currentVersion;
+ }
+
+ // TODO required for jdbi binder
+ public boolean getIsActive() {
+ return isActive;
+ }
+
+ public boolean isActive() {
+ return isActive;
+ }
+
+ public void setTotalOrdering(final long totalOrdering) {
+ this.totalOrdering = totalOrdering;
+ }
+
+ public void setEventType(final EventType eventType) {
+ this.eventType = eventType;
+ }
+
+ public void setUserType(final ApiEventType userType) {
+ this.userType = userType;
+ }
+
+ public void setRequestedDate(final DateTime requestedDate) {
+ this.requestedDate = requestedDate;
+ }
+
+ public void setEffectiveDate(final DateTime effectiveDate) {
+ this.effectiveDate = effectiveDate;
+ }
+
+ public void setSubscriptionId(final UUID subscriptionId) {
+ this.subscriptionId = subscriptionId;
+ }
+
+ public void setPlanName(final String planName) {
+ this.planName = planName;
+ }
+
+ public void setPhaseName(final String phaseName) {
+ this.phaseName = phaseName;
+ }
+
+ public void setPriceListName(final String priceListName) {
+ this.priceListName = priceListName;
+ }
+
+ public void setCurrentVersion(final long currentVersion) {
+ this.currentVersion = currentVersion;
+ }
+
+ public void setIsActive(final boolean isActive) {
+ this.isActive = isActive;
+ }
+
+ public static SubscriptionBaseEvent toSubscriptionEvent(final SubscriptionEventModelDao src) {
+
+ if (src == null) {
+ return null;
+ }
+
+ final EventBaseBuilder<?> base = ((src.getEventType() == EventType.PHASE) ?
+ new PhaseEventBuilder() :
+ new ApiEventBuilder())
+ .setTotalOrdering(src.getTotalOrdering())
+ .setUuid(src.getId())
+ .setSubscriptionId(src.getSubscriptionId())
+ .setCreatedDate(src.getCreatedDate())
+ .setUpdatedDate(src.getUpdatedDate())
+ .setRequestedDate(src.getRequestedDate())
+ .setEffectiveDate(src.getEffectiveDate())
+ .setProcessedDate(src.getCreatedDate())
+ .setActiveVersion(src.getCurrentVersion())
+ .setActive(src.isActive());
+
+ SubscriptionBaseEvent result = null;
+ if (src.getEventType() == EventType.PHASE) {
+ result = new PhaseEventData(new PhaseEventBuilder(base).setPhaseName(src.getPhaseName()));
+ } else if (src.getEventType() == EventType.API_USER) {
+ final ApiEventBuilder builder = new ApiEventBuilder(base)
+ .setEventPlan(src.getPlanName())
+ .setEventPlanPhase(src.getPhaseName())
+ .setEventPriceList(src.getPriceListName())
+ .setEventType(src.getUserType())
+ .setFromDisk(true);
+
+ if (src.getUserType() == ApiEventType.CREATE) {
+ result = new ApiEventCreate(builder);
+ } else if (src.getUserType() == ApiEventType.RE_CREATE) {
+ result = new ApiEventReCreate(builder);
+ } else if (src.getUserType() == ApiEventType.MIGRATE_ENTITLEMENT) {
+ result = new ApiEventMigrateSubscription(builder);
+ } else if (src.getUserType() == ApiEventType.MIGRATE_BILLING) {
+ result = new ApiEventMigrateBilling(builder);
+ } else if (src.getUserType() == ApiEventType.TRANSFER) {
+ result = new ApiEventTransfer(builder);
+ } else if (src.getUserType() == ApiEventType.CHANGE) {
+ result = new ApiEventChange(builder);
+ } else if (src.getUserType() == ApiEventType.CANCEL) {
+ result = new ApiEventCancel(builder);
+ } else if (src.getUserType() == ApiEventType.RE_CREATE) {
+ result = new ApiEventReCreate(builder);
+ } else if (src.getUserType() == ApiEventType.UNCANCEL) {
+ result = new ApiEventUncancel(builder);
+ }
+ } else {
+ throw new SubscriptionBaseError(String.format("Can't figure out event %s", src.getEventType()));
+ }
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SubscriptionEventModelDao");
+ sb.append("{totalOrdering=").append(totalOrdering);
+ sb.append(", eventType=").append(eventType);
+ sb.append(", userType=").append(userType);
+ sb.append(", requestedDate=").append(requestedDate);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", subscriptionId=").append(subscriptionId);
+ sb.append(", planName='").append(planName).append('\'');
+ sb.append(", phaseName='").append(phaseName).append('\'');
+ sb.append(", priceListName='").append(priceListName).append('\'');
+ sb.append(", currentVersion=").append(currentVersion);
+ sb.append(", isActive=").append(isActive);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final SubscriptionEventModelDao that = (SubscriptionEventModelDao) o;
+
+ if (currentVersion != that.currentVersion) {
+ return false;
+ }
+ if (isActive != that.isActive) {
+ return false;
+ }
+ if (totalOrdering != that.totalOrdering) {
+ return false;
+ }
+ if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+ return false;
+ }
+ if (eventType != that.eventType) {
+ return false;
+ }
+ if (phaseName != null ? !phaseName.equals(that.phaseName) : that.phaseName != null) {
+ return false;
+ }
+ if (planName != null ? !planName.equals(that.planName) : that.planName != null) {
+ return false;
+ }
+ if (priceListName != null ? !priceListName.equals(that.priceListName) : that.priceListName != null) {
+ return false;
+ }
+ if (requestedDate != null ? !requestedDate.equals(that.requestedDate) : that.requestedDate != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+ if (userType != that.userType) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (int) (totalOrdering ^ (totalOrdering >>> 32));
+ result = 31 * result + (eventType != null ? eventType.hashCode() : 0);
+ result = 31 * result + (userType != null ? userType.hashCode() : 0);
+ result = 31 * result + (requestedDate != null ? requestedDate.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ result = 31 * result + (subscriptionId != null ? subscriptionId.hashCode() : 0);
+ result = 31 * result + (planName != null ? planName.hashCode() : 0);
+ result = 31 * result + (phaseName != null ? phaseName.hashCode() : 0);
+ result = 31 * result + (priceListName != null ? priceListName.hashCode() : 0);
+ result = 31 * result + (int) (currentVersion ^ (currentVersion >>> 32));
+ result = 31 * result + (isActive ? 1 : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.SUBSCRIPTION_EVENTS;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return null;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionModelDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionModelDao.java
new file mode 100644
index 0000000..0e09ec5
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/model/SubscriptionModelDao.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.dao.model;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class SubscriptionModelDao extends EntityBase implements EntityModelDao<SubscriptionBase> {
+
+ private UUID bundleId;
+ private ProductCategory category;
+ private DateTime startDate;
+ private DateTime bundleStartDate;
+ private long activeVersion;
+ private DateTime chargedThroughDate;
+
+ public SubscriptionModelDao() { /* For the DAO mapper */ }
+
+ public SubscriptionModelDao(final UUID id, final UUID bundleId, final ProductCategory category, final DateTime startDate, final DateTime bundleStartDate,
+ final long activeVersion, final DateTime chargedThroughDate, final DateTime createdDate, final DateTime updateDate) {
+ super(id, createdDate, updateDate);
+ this.bundleId = bundleId;
+ this.category = category;
+ this.startDate = startDate;
+ this.bundleStartDate = bundleStartDate;
+ this.activeVersion = activeVersion;
+ this.chargedThroughDate = chargedThroughDate;
+ }
+
+ public SubscriptionModelDao(final DefaultSubscriptionBase src) {
+ this(src.getId(), src.getBundleId(), src.getCategory(), src.getAlignStartDate(), src.getBundleStartDate(), src.getActiveVersion(),
+ src.getChargedThroughDate(), src.getCreatedDate(), src.getUpdatedDate());
+ }
+
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ public ProductCategory getCategory() {
+ return category;
+ }
+
+ public DateTime getStartDate() {
+ return startDate;
+ }
+
+ public DateTime getBundleStartDate() {
+ return bundleStartDate;
+ }
+
+ public long getActiveVersion() {
+ return activeVersion;
+ }
+
+ public DateTime getChargedThroughDate() {
+ return chargedThroughDate;
+ }
+
+ public void setBundleId(final UUID bundleId) {
+ this.bundleId = bundleId;
+ }
+
+ public void setCategory(final ProductCategory category) {
+ this.category = category;
+ }
+
+ public void setStartDate(final DateTime startDate) {
+ this.startDate = startDate;
+ }
+
+ public void setBundleStartDate(final DateTime bundleStartDate) {
+ this.bundleStartDate = bundleStartDate;
+ }
+
+ public void setActiveVersion(final long activeVersion) {
+ this.activeVersion = activeVersion;
+ }
+
+ public void setChargedThroughDate(final DateTime chargedThroughDate) {
+ this.chargedThroughDate = chargedThroughDate;
+ }
+
+ public static SubscriptionBase toSubscription(final SubscriptionModelDao src) {
+ if (src == null) {
+ return null;
+ }
+ return new DefaultSubscriptionBase(new SubscriptionBuilder()
+ .setId(src.getId())
+ .setBundleId(src.getBundleId())
+ .setCategory(src.getCategory())
+ .setCreatedDate(src.getCreatedDate())
+ .setUpdatedDate(src.getUpdatedDate())
+ .setBundleStartDate(src.getBundleStartDate())
+ .setAlignStartDate(src.getStartDate())
+ .setActiveVersion(src.getActiveVersion())
+ .setChargedThroughDate(src.getChargedThroughDate())
+ .setCreatedDate(src.getCreatedDate())
+ .setUpdatedDate(src.getUpdatedDate()));
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SubscriptionModelDao");
+ sb.append("{bundleId=").append(bundleId);
+ sb.append(", category=").append(category);
+ sb.append(", startDate=").append(startDate);
+ sb.append(", bundleStartDate=").append(bundleStartDate);
+ sb.append(", activeVersion=").append(activeVersion);
+ sb.append(", chargedThroughDate=").append(chargedThroughDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final SubscriptionModelDao that = (SubscriptionModelDao) o;
+
+ if (activeVersion != that.activeVersion) {
+ return false;
+ }
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
+ }
+ if (bundleStartDate != null ? !bundleStartDate.equals(that.bundleStartDate) : that.bundleStartDate != null) {
+ return false;
+ }
+ if (category != that.category) {
+ return false;
+ }
+ if (chargedThroughDate != null ? !chargedThroughDate.equals(that.chargedThroughDate) : that.chargedThroughDate != null) {
+ return false;
+ }
+ if (startDate != null ? !startDate.equals(that.startDate) : that.startDate != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (category != null ? category.hashCode() : 0);
+ result = 31 * result + (startDate != null ? startDate.hashCode() : 0);
+ result = 31 * result + (bundleStartDate != null ? bundleStartDate.hashCode() : 0);
+ result = 31 * result + (int) (activeVersion ^ (activeVersion >>> 32));
+ result = 31 * result + (chargedThroughDate != null ? chargedThroughDate.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.SUBSCRIPTIONS;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return null;
+ }
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java
new file mode 100644
index 0000000..0329399
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/RepairSubscriptionDao.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.dao;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.SubscriptionApiException;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData.BundleMigrationData;
+import org.killbill.billing.subscription.api.timeline.RepairSubscriptionLifecycleDao;
+import org.killbill.billing.subscription.api.timeline.SubscriptionDataRepair;
+import org.killbill.billing.subscription.api.transfer.TransferCancelData;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.EntityDaoBase;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+
+public class RepairSubscriptionDao extends EntityDaoBase<SubscriptionBundleModelDao, SubscriptionBaseBundle, SubscriptionApiException> implements SubscriptionDao, RepairSubscriptionLifecycleDao {
+
+ private static final String NOT_IMPLEMENTED = "Not implemented";
+
+ private final ThreadLocal<Map<UUID, SubscriptionRepairEvent>> preThreadsInRepairSubscriptions = new ThreadLocal<Map<UUID, SubscriptionRepairEvent>>();
+
+ @Inject
+ public RepairSubscriptionDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao), BundleSqlDao.class);
+ }
+
+ @Override
+ protected SubscriptionApiException generateAlreadyExistsException(final SubscriptionBundleModelDao entity, final InternalCallContext context) {
+ return new SubscriptionApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, entity.getExternalKey());
+ }
+
+ private static final class SubscriptionEventWithOrderingId {
+
+ private final SubscriptionBaseEvent event;
+ private final long orderingId;
+
+ public SubscriptionEventWithOrderingId(final SubscriptionBaseEvent event, final long orderingId) {
+ this.event = event;
+ this.orderingId = orderingId;
+ }
+
+ public SubscriptionBaseEvent getEvent() {
+ return event;
+ }
+
+ public long getOrderingId() {
+ return orderingId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder tmp = new StringBuilder();
+ tmp.append("[");
+ tmp.append(event.getType());
+ tmp.append(": effDate=");
+ tmp.append(event.getEffectiveDate());
+ tmp.append(", subId=");
+ tmp.append(event.getSubscriptionId());
+ tmp.append(", ordering=");
+ tmp.append(event.getTotalOrdering());
+ tmp.append("]");
+ return tmp.toString();
+ }
+ }
+
+ private static final class SubscriptionRepairEvent {
+
+ private final Set<SubscriptionEventWithOrderingId> events;
+ private long curOrderingId;
+
+ public SubscriptionRepairEvent(final List<SubscriptionBaseEvent> initialEvents) {
+ this.events = new TreeSet<SubscriptionEventWithOrderingId>(new Comparator<SubscriptionEventWithOrderingId>() {
+ @Override
+ public int compare(final SubscriptionEventWithOrderingId o1, final SubscriptionEventWithOrderingId o2) {
+ // Work around jdk7 change: compare(o1, o1) is now invoked when inserting the first element
+ // See:
+ // - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5045147
+ // - http://hg.openjdk.java.net/jdk7/tl/jdk/rev/bf37edb38fbb
+ if (o1 == o2) {
+ return 0;
+ }
+
+ final int result = o1.getEvent().getEffectiveDate().compareTo(o2.getEvent().getEffectiveDate());
+ if (result == 0) {
+ if (o1.getOrderingId() < o2.getOrderingId()) {
+ return -1;
+ } else if (o1.getOrderingId() > o2.getOrderingId()) {
+ return 1;
+ } else {
+ throw new RuntimeException(String.format(" Repair subscription events should not have the same orderingId %s, %s ", o1, o2));
+ }
+ }
+ return result;
+ }
+ });
+
+ this.curOrderingId = 0;
+
+ if (initialEvents != null) {
+ addEvents(initialEvents);
+ }
+ }
+
+ public List<SubscriptionBaseEvent> getEvents() {
+ return new ArrayList<SubscriptionBaseEvent>(Collections2.transform(events, new Function<SubscriptionEventWithOrderingId, SubscriptionBaseEvent>() {
+ @Override
+ public SubscriptionBaseEvent apply(SubscriptionEventWithOrderingId in) {
+ return in.getEvent();
+ }
+ }));
+ }
+
+ public void addEvents(final List<SubscriptionBaseEvent> newEvents) {
+ for (final SubscriptionBaseEvent cur : newEvents) {
+ events.add(new SubscriptionEventWithOrderingId(cur, curOrderingId++));
+ }
+ }
+ }
+
+ private Map<UUID, SubscriptionRepairEvent> getRepairMap() {
+ if (preThreadsInRepairSubscriptions.get() == null) {
+ preThreadsInRepairSubscriptions.set(new HashMap<UUID, SubscriptionRepairEvent>());
+ }
+ return preThreadsInRepairSubscriptions.get();
+ }
+
+ private SubscriptionRepairEvent getRepairSubscriptionEvents(final UUID subscriptionId) {
+ final Map<UUID, SubscriptionRepairEvent> map = getRepairMap();
+ return map.get(subscriptionId);
+ }
+
+ @Override
+ public List<SubscriptionBaseEvent> getEventsForSubscription(final UUID subscriptionId, final InternalTenantContext context) {
+ final SubscriptionRepairEvent target = getRepairSubscriptionEvents(subscriptionId);
+ return new LinkedList<SubscriptionBaseEvent>(target.getEvents());
+ }
+
+ @Override
+ public void createSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> createEvents, final InternalCallContext context) {
+ addEvents(subscription.getId(), createEvents);
+ }
+
+ @Override
+ public void recreateSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> recreateEvents, final InternalCallContext context) {
+ addEvents(subscription.getId(), recreateEvents);
+ }
+
+ @Override
+ public void cancelSubscription(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent cancelEvent, final InternalCallContext context, final int cancelSeq) {
+ final UUID subscriptionId = subscription.getId();
+ final long activeVersion = cancelEvent.getActiveVersion();
+ addEvents(subscriptionId, Collections.singletonList(cancelEvent));
+ final SubscriptionRepairEvent target = getRepairSubscriptionEvents(subscriptionId);
+ boolean foundCancelEvent = false;
+ for (final SubscriptionBaseEvent cur : target.getEvents()) {
+ if (cur.getId().equals(cancelEvent.getId())) {
+ foundCancelEvent = true;
+ } else if (foundCancelEvent) {
+ cur.setActiveVersion(activeVersion - 1);
+ }
+ }
+ }
+
+ @Override
+ public void cancelSubscriptions(final List<DefaultSubscriptionBase> subscriptions, final List<SubscriptionBaseEvent> cancelEvents, final InternalCallContext context) {
+ }
+
+ @Override
+ public void changePlan(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> changeEvents, final InternalCallContext context) {
+ addEvents(subscription.getId(), changeEvents);
+ }
+
+ @Override
+ public void initializeRepair(final UUID subscriptionId, final List<SubscriptionBaseEvent> initialEvents, final InternalTenantContext context) {
+ final Map<UUID, SubscriptionRepairEvent> map = getRepairMap();
+ if (map.get(subscriptionId) == null) {
+ final SubscriptionRepairEvent value = new SubscriptionRepairEvent(initialEvents);
+ map.put(subscriptionId, value);
+ } else {
+ throw new SubscriptionBaseError(String.format("Unexpected SubscriptionRepairEvent %s for thread %s", subscriptionId, Thread.currentThread().getName()));
+ }
+ }
+
+ @Override
+ public void cleanup(final InternalTenantContext context) {
+ final Map<UUID, SubscriptionRepairEvent> map = getRepairMap();
+ map.clear();
+ }
+
+ private void addEvents(final UUID subscriptionId, final List<SubscriptionBaseEvent> events) {
+ final SubscriptionRepairEvent target = getRepairSubscriptionEvents(subscriptionId);
+ target.addEvents(events);
+ }
+
+ @Override
+ public void uncancelSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> uncancelEvents, final InternalCallContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public List<SubscriptionBaseBundle> getSubscriptionBundleForAccount(final UUID accountId, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public SubscriptionBaseBundle getSubscriptionBundleFromId(final UUID bundleId, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public SubscriptionBaseBundle createSubscriptionBundle(final DefaultSubscriptionBaseBundle bundle, final InternalCallContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public SubscriptionBase getSubscriptionFromId(final UUID subscriptionId, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public UUID getAccountIdFromSubscriptionId(final UUID subscriptionId, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public SubscriptionBase getBaseSubscription(final UUID bundleId, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public List<SubscriptionBase> getSubscriptions(final UUID bundleId, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public void updateChargedThroughDate(final DefaultSubscriptionBase subscription, final InternalCallContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public void createNextPhaseEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent nextPhase, final InternalCallContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public SubscriptionBaseEvent getEventById(final UUID eventId, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public Map<UUID, List<SubscriptionBase>> getSubscriptionsForAccount(final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public Map<UUID, List<SubscriptionBaseEvent>> getEventsForBundle(final UUID bundleId, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public List<SubscriptionBaseEvent> getPendingEventsForSubscription(final UUID subscriptionId, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public void migrate(final UUID accountId, final AccountMigrationData data, final InternalCallContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public void repair(final UUID accountId, final UUID bundleId, final List<SubscriptionDataRepair> inRepair, final InternalCallContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public void transfer(final UUID srcAccountId, final UUID destAccountId, final BundleMigrationData data,
+ final List<TransferCancelData> transferCancelData, final InternalCallContext fromContext,
+ final InternalCallContext toContext) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public void updateBundleExternalKey(final UUID bundleId, final String externalKey, final InternalCallContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public List<SubscriptionBaseBundle> getSubscriptionBundlesForKey(final String bundleKey, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public Pagination<SubscriptionBundleModelDao> searchSubscriptionBundles(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public List<UUID> getNonAOSubscriptionIdsForKey(final String bundleKey, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+
+ @Override
+ public List<SubscriptionBaseBundle> getSubscriptionBundlesForAccountAndKey(final UUID accountId, final String bundleKey, final InternalTenantContext context) {
+ throw new SubscriptionBaseError(NOT_IMPLEMENTED);
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
new file mode 100644
index 0000000..797c23d
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.dao;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entitlement.api.SubscriptionApiException;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData.BundleMigrationData;
+import org.killbill.billing.subscription.api.timeline.SubscriptionDataRepair;
+import org.killbill.billing.subscription.api.transfer.TransferCancelData;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.EntityDao;
+
+public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, SubscriptionBaseBundle, SubscriptionApiException> {
+
+ // Bundle apis
+ public List<SubscriptionBaseBundle> getSubscriptionBundleForAccount(UUID accountId, InternalTenantContext context);
+
+ public List<SubscriptionBaseBundle> getSubscriptionBundlesForKey(String bundleKey, InternalTenantContext context);
+
+ public Pagination<SubscriptionBundleModelDao> searchSubscriptionBundles(String searchKey, Long offset, Long limit, InternalTenantContext context);
+
+ public Iterable<UUID> getNonAOSubscriptionIdsForKey(String bundleKey, InternalTenantContext context);
+
+ public List<SubscriptionBaseBundle> getSubscriptionBundlesForAccountAndKey(UUID accountId, String bundleKey, InternalTenantContext context);
+
+ public SubscriptionBaseBundle getSubscriptionBundleFromId(UUID bundleId, InternalTenantContext context);
+
+ public SubscriptionBaseBundle createSubscriptionBundle(DefaultSubscriptionBaseBundle bundle, InternalCallContext context);
+
+ public SubscriptionBase getSubscriptionFromId(UUID subscriptionId, InternalTenantContext context);
+
+ // ACCOUNT retrieval
+ public UUID getAccountIdFromSubscriptionId(UUID subscriptionId, InternalTenantContext context);
+
+ // SubscriptionBase retrieval
+ public SubscriptionBase getBaseSubscription(UUID bundleId, InternalTenantContext context);
+
+ public List<SubscriptionBase> getSubscriptions(UUID bundleId, InternalTenantContext context);
+
+ public Map<UUID, List<SubscriptionBase>> getSubscriptionsForAccount(InternalTenantContext context);
+
+ // Update
+ public void updateChargedThroughDate(DefaultSubscriptionBase subscription, InternalCallContext context);
+
+ // Event apis
+ public void createNextPhaseEvent(DefaultSubscriptionBase subscription, SubscriptionBaseEvent nextPhase, InternalCallContext context);
+
+ public SubscriptionBaseEvent getEventById(UUID eventId, InternalTenantContext context);
+
+ public Map<UUID, List<SubscriptionBaseEvent>> getEventsForBundle(UUID bundleId, InternalTenantContext context);
+
+ public List<SubscriptionBaseEvent> getEventsForSubscription(UUID subscriptionId, InternalTenantContext context);
+
+ public List<SubscriptionBaseEvent> getPendingEventsForSubscription(UUID subscriptionId, InternalTenantContext context);
+
+ // SubscriptionBase creation, cancellation, changePlanWithRequestedDate apis
+ public void createSubscription(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> initialEvents, InternalCallContext context);
+
+ public void recreateSubscription(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> recreateEvents, InternalCallContext context);
+
+ public void cancelSubscription(DefaultSubscriptionBase subscription, SubscriptionBaseEvent cancelEvent, InternalCallContext context, int cancelSeq);
+
+ public void cancelSubscriptions(List<DefaultSubscriptionBase> subscriptions, List<SubscriptionBaseEvent> cancelEvents, InternalCallContext context);
+
+ public void uncancelSubscription(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> uncancelEvents, InternalCallContext context);
+
+ public void changePlan(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> changeEvents, InternalCallContext context);
+
+ public void migrate(UUID accountId, AccountMigrationData data, InternalCallContext context);
+
+ public void transfer(UUID srcAccountId, UUID destAccountId, BundleMigrationData data, List<TransferCancelData> transferCancelData, InternalCallContext fromContext, InternalCallContext toContext);
+
+ public void updateBundleExternalKey(UUID bundleId, String externalKey, InternalCallContext context);
+
+ // Repair
+ public void repair(UUID accountId, UUID bundleId, List<SubscriptionDataRepair> inRepair, InternalCallContext context);
+}
+
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
new file mode 100644
index 0000000..dd4656a
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.dao;
+
+import java.util.Date;
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.subscription.engine.dao.model.SubscriptionEventModelDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface SubscriptionEventSqlDao extends EntitySqlDao<SubscriptionEventModelDao, SubscriptionBaseEvent> {
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ public void unactiveEvent(@Bind("id") String id,
+ @BindBean final InternalCallContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ public void reactiveEvent(@Bind("id") String id,
+ @BindBean final InternalCallContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ public void updateVersion(@Bind("id") String id,
+ @Bind("currentVersion") Long currentVersion,
+ @BindBean final InternalCallContext context);
+
+ @SqlQuery
+ public List<SubscriptionEventModelDao> getFutureActiveEventForSubscription(@Bind("subscriptionId") String subscriptionId,
+ @Bind("now") Date now,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public List<SubscriptionEventModelDao> getEventsForSubscription(@Bind("subscriptionId") String subscriptionId,
+ @BindBean final InternalTenantContext context);
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionSqlDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionSqlDao.java
new file mode 100644
index 0000000..76baf12
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionSqlDao.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.dao;
+
+import java.util.Date;
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.subscription.engine.dao.model.SubscriptionModelDao;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface SubscriptionSqlDao extends EntitySqlDao<SubscriptionModelDao, SubscriptionBase> {
+
+ @SqlQuery
+ public List<SubscriptionModelDao> getSubscriptionsFromBundleId(@Bind("bundleId") String bundleId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ public void updateChargedThroughDate(@Bind("id") String id, @Bind("chargedThroughDate") Date chargedThroughDate,
+ @BindBean final InternalCallContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ void updateActiveVersion(@Bind("id") String id, @Bind("activeVersion") long activeVersion,
+ @BindBean final InternalCallContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.UPDATE)
+ public void updateForRepair(@Bind("id") String id, @Bind("activeVersion") long activeVersion,
+ @Bind("startDate") Date startDate,
+ @Bind("bundleStartDate") Date bundleStartDate,
+ @BindBean final InternalCallContext context);
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/EventBase.java b/subscription/src/main/java/org/killbill/billing/subscription/events/EventBase.java
new file mode 100644
index 0000000..de34a88
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/EventBase.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.subscription.events.user.ApiEvent;
+
+public abstract class EventBase implements SubscriptionBaseEvent {
+
+ private final long totalOrdering;
+ private final UUID uuid;
+ private final UUID subscriptionId;
+ private final DateTime createdDate;
+ private final DateTime updatedDate;
+ private final DateTime requestedDate;
+ private final DateTime effectiveDate;
+ private final DateTime processedDate;
+
+ private long activeVersion;
+ private boolean isActive;
+
+ public EventBase(final EventBaseBuilder<?> builder) {
+ this.totalOrdering = builder.getTotalOrdering();
+ this.uuid = builder.getUuid();
+ this.subscriptionId = builder.getSubscriptionId();
+ this.createdDate = builder.getCreatedDate();
+ this.updatedDate = builder.getUpdatedDate();
+ this.requestedDate = builder.getRequestedDate();
+ this.effectiveDate = builder.getEffectiveDate();
+ this.processedDate = builder.getProcessedDate();
+ this.activeVersion = builder.getActiveVersion();
+ this.isActive = builder.isActive();
+ }
+
+ @Override
+ public DateTime getRequestedDate() {
+ return requestedDate;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public DateTime getProcessedDate() {
+ return processedDate;
+ }
+
+ @Override
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ @Override
+ public long getTotalOrdering() {
+ return totalOrdering;
+ }
+
+ @Override
+ public UUID getId() {
+ return uuid;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return createdDate;
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return updatedDate;
+ }
+
+ @Override
+ public long getActiveVersion() {
+ return activeVersion;
+ }
+
+ @Override
+ public void setActiveVersion(final long activeVersion) {
+ this.activeVersion = activeVersion;
+ }
+
+ @Override
+ public boolean isActive() {
+ return isActive;
+ }
+
+ @Override
+ public void deactivate() {
+ this.isActive = false;
+ }
+
+ @Override
+ public void reactivate() {
+ this.isActive = true;
+ }
+
+ //
+ // Really used for unit tests only as the sql implementation relies on date first and then event insertion
+ //
+ // Order first by:
+ // - effectiveDate, followed by processedDate, requestedDate
+ // - if all dates are equal-- unlikely, we first return PHASE EVENTS
+ // - If both events are User events, return the first CREATE, CHANGE,... as specified by ApiEventType
+ // - If all that is not enough return consistent by random ordering based on UUID
+ //
+ @Override
+ public int compareTo(final SubscriptionBaseEvent other) {
+ if (other == null) {
+ throw new IllegalArgumentException("IEvent is compared to a null instance");
+ }
+
+ if (effectiveDate.isBefore(other.getEffectiveDate())) {
+ return -1;
+ } else if (effectiveDate.isAfter(other.getEffectiveDate())) {
+ return 1;
+ } else if (processedDate.isBefore(other.getProcessedDate())) {
+ return -1;
+ } else if (processedDate.isAfter(other.getProcessedDate())) {
+ return 1;
+ } else if (requestedDate.isBefore(other.getRequestedDate())) {
+ return -1;
+ } else if (requestedDate.isAfter(other.getRequestedDate())) {
+ return 1;
+ } else if (getType() != other.getType()) {
+ return (getType() == EventType.PHASE) ? -1 : 1;
+ } else if (getType() == EventType.API_USER) {
+ return ((ApiEvent) this).getEventType().compareTo(((ApiEvent) other).getEventType());
+ } else {
+ return uuid.compareTo(other.getId());
+ }
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (!(other instanceof SubscriptionBaseEvent)) {
+ return false;
+ }
+ return (this.compareTo((SubscriptionBaseEvent) other) == 0);
+ }
+
+ @Override
+ public abstract EventType getType();
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/EventBaseBuilder.java b/subscription/src/main/java/org/killbill/billing/subscription/events/EventBaseBuilder.java
new file mode 100644
index 0000000..280abe7
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/EventBaseBuilder.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+@SuppressWarnings("unchecked")
+public class EventBaseBuilder<T extends EventBaseBuilder<T>> {
+
+ private long totalOrdering;
+ private UUID uuid;
+ private UUID subscriptionId;
+ private DateTime createdDate;
+ private DateTime updatedDate;
+ private DateTime requestedDate;
+ private DateTime effectiveDate;
+ private DateTime processedDate;
+
+ private long activeVersion;
+ private boolean isActive;
+
+ public EventBaseBuilder() {
+ this.uuid = UUID.randomUUID();
+ this.isActive = true;
+ }
+
+ public EventBaseBuilder(final EventBaseBuilder<?> copy) {
+ this.uuid = copy.uuid;
+ this.subscriptionId = copy.subscriptionId;
+ this.requestedDate = copy.requestedDate;
+ this.effectiveDate = copy.effectiveDate;
+ this.processedDate = copy.processedDate;
+ this.createdDate = copy.getCreatedDate();
+ this.activeVersion = copy.activeVersion;
+ this.isActive = copy.isActive;
+ this.totalOrdering = copy.totalOrdering;
+ }
+
+ public T setTotalOrdering(final long totalOrdering) {
+ this.totalOrdering = totalOrdering;
+ return (T) this;
+ }
+
+ public T setUuid(final UUID uuid) {
+ this.uuid = uuid;
+ return (T) this;
+ }
+
+ public T setCreatedDate(final DateTime createdDate) {
+ this.createdDate = createdDate;
+ return (T) this;
+ }
+
+ public T setUpdatedDate(final DateTime updatedDate) {
+ this.updatedDate = updatedDate;
+ return (T) this;
+ }
+
+ public T setSubscriptionId(final UUID subscriptionId) {
+ this.subscriptionId = subscriptionId;
+ return (T) this;
+ }
+
+ public T setRequestedDate(final DateTime requestedDate) {
+ this.requestedDate = requestedDate;
+ return (T) this;
+ }
+
+ public T setEffectiveDate(final DateTime effectiveDate) {
+ this.effectiveDate = effectiveDate;
+ return (T) this;
+ }
+
+ public T setProcessedDate(final DateTime processedDate) {
+ this.processedDate = processedDate;
+ return (T) this;
+ }
+
+ public T setActiveVersion(final long activeVersion) {
+ this.activeVersion = activeVersion;
+ return (T) this;
+ }
+
+ public T setActive(final boolean isActive) {
+ this.isActive = isActive;
+ return (T) this;
+ }
+
+ public long getTotalOrdering() {
+ return totalOrdering;
+ }
+
+ public UUID getUuid() {
+ return uuid;
+ }
+
+ public DateTime getCreatedDate() {
+ return createdDate;
+ }
+
+ public DateTime getUpdatedDate() {
+ return updatedDate;
+ }
+
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ public DateTime getRequestedDate() {
+ return requestedDate;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public DateTime getProcessedDate() {
+ return processedDate;
+ }
+
+ public long getActiveVersion() {
+ return activeVersion;
+ }
+
+ public boolean isActive() {
+ return isActive;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEvent.java
new file mode 100644
index 0000000..76fac34
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEvent.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.phase;
+
+
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+
+public interface PhaseEvent extends SubscriptionBaseEvent {
+
+ public String getPhase();
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventBuilder.java b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventBuilder.java
new file mode 100644
index 0000000..21f22e8
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventBuilder.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.phase;
+
+
+import org.killbill.billing.subscription.events.EventBaseBuilder;
+
+public class PhaseEventBuilder extends EventBaseBuilder<PhaseEventBuilder> {
+
+ private String phaseName;
+
+ public PhaseEventBuilder() {
+ super();
+ }
+
+ public PhaseEventBuilder(final EventBaseBuilder<?> base) {
+ super(base);
+ }
+
+ public PhaseEventBuilder setPhaseName(final String phaseName) {
+ this.phaseName = phaseName;
+ return this;
+ }
+
+ public String getPhaseName() {
+ return phaseName;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java
new file mode 100644
index 0000000..6af6942
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/phase/PhaseEventData.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.phase;
+
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.events.EventBase;
+
+
+public class PhaseEventData extends EventBase implements PhaseEvent {
+
+ private final String phaseName;
+
+ public PhaseEventData(final PhaseEventBuilder builder) {
+ super(builder);
+ this.phaseName = builder.getPhaseName();
+ }
+
+ @Override
+ public EventType getType() {
+ return EventType.PHASE;
+ }
+
+ @Override
+ public String getPhase() {
+ return phaseName;
+ }
+
+ @Override
+ public String toString() {
+ return "PhaseEvent [getId()= " + getId()
+ + ", phaseName=" + phaseName
+ + ", getType()=" + getType()
+ + ", getPhase()=" + getPhase()
+ + ", getRequestedDate()=" + getRequestedDate()
+ + ", getEffectiveDate()=" + getEffectiveDate()
+ + ", getActiveVersion()=" + getActiveVersion()
+ + ", getProcessedDate()=" + getProcessedDate()
+ + ", getSubscriptionId()=" + getSubscriptionId()
+ + ", isActive()=" + isActive() + "]\n";
+ }
+
+ public static PhaseEvent createNextPhaseEvent(final String phaseName, final DefaultSubscriptionBase subscription, final DateTime now, final DateTime effectiveDate) {
+ return (phaseName == null) ?
+ null :
+ new PhaseEventData(new PhaseEventBuilder()
+ .setSubscriptionId(subscription.getId())
+ .setRequestedDate(now)
+ .setEffectiveDate(effectiveDate)
+ .setProcessedDate(now)
+ .setActiveVersion(subscription.getActiveVersion())
+ .setPhaseName(phaseName));
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java
new file mode 100644
index 0000000..990ba4f
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/SubscriptionBaseEvent.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.util.entity.Entity;
+
+
+public interface SubscriptionBaseEvent extends Comparable<SubscriptionBaseEvent>, Entity {
+
+ public enum EventType {
+ API_USER,
+ PHASE
+ }
+
+ public EventType getType();
+
+ public long getTotalOrdering();
+
+ public long getActiveVersion();
+
+ public void setActiveVersion(long activeVersion);
+
+ public boolean isActive();
+
+ public void deactivate();
+
+ public void reactivate();
+
+ public DateTime getProcessedDate();
+
+ public DateTime getRequestedDate();
+
+ public DateTime getEffectiveDate();
+
+ public UUID getSubscriptionId();
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEvent.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEvent.java
new file mode 100644
index 0000000..a1bf77f
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEvent.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+
+
+public interface ApiEvent extends SubscriptionBaseEvent {
+
+ public String getEventPlan();
+
+ public String getEventPlanPhase();
+
+ public ApiEventType getEventType();
+
+ public String getPriceList();
+
+ public boolean isFromDisk();
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBase.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBase.java
new file mode 100644
index 0000000..ce462fe
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBase.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+import org.killbill.billing.subscription.events.EventBase;
+
+public class ApiEventBase extends EventBase implements ApiEvent {
+
+ private final ApiEventType eventType;
+ // Only valid for CREATE/CHANGE
+ private final String eventPlan;
+ private final String eventPlanPhase;
+ private final String eventPriceList;
+ private final boolean fromDisk;
+
+ public ApiEventBase(final ApiEventBuilder builder) {
+ super(builder);
+ this.eventType = builder.getEventType();
+ this.eventPriceList = builder.getEventPriceList();
+ this.eventPlan = builder.getEventPlan();
+ this.eventPlanPhase = builder.getEventPlanPhase();
+ this.fromDisk = builder.isFromDisk();
+ }
+
+ @Override
+ public ApiEventType getEventType() {
+ return eventType;
+ }
+
+ @Override
+ public String getEventPlan() {
+ return eventPlan;
+ }
+
+ @Override
+ public String getEventPlanPhase() {
+ return eventPlanPhase;
+ }
+
+ @Override
+ public EventType getType() {
+ return EventType.API_USER;
+ }
+
+ @Override
+ public String getPriceList() {
+ return eventPriceList;
+ }
+
+ @Override
+ public boolean isFromDisk() {
+ return fromDisk;
+ }
+
+
+ @Override
+ public String toString() {
+ return "ApiEventBase [ getId()= " + getId()
+ + " eventType=" + eventType
+ + ", eventPlan=" + eventPlan
+ + ", eventPlanPhase=" + eventPlanPhase
+ + ", getEventType()=" + getEventType()
+ + ", getEventPlan()=" + getEventPlan()
+ + ", getEventPlanPhase()=" + getEventPlanPhase()
+ + ", getType()=" + getType()
+ + ", getRequestedDate()=" + getRequestedDate()
+ + ", getEffectiveDate()=" + getEffectiveDate()
+ + ", getActiveVersion()=" + getActiveVersion()
+ + ", getProcessedDate()=" + getProcessedDate()
+ + ", getSubscriptionId()=" + getSubscriptionId()
+ + ", isActive()=" + isActive() + "]";
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java
new file mode 100644
index 0000000..2568e76
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+import org.killbill.billing.subscription.events.EventBaseBuilder;
+
+
+public class ApiEventBuilder extends EventBaseBuilder<ApiEventBuilder> {
+
+ private ApiEventType eventType;
+ private String eventPlan;
+ private String eventPlanPhase;
+ private String eventPriceList;
+ private boolean fromDisk;
+
+
+ public ApiEventBuilder() {
+ super();
+ }
+
+ public ApiEventBuilder(final EventBaseBuilder<?> base) {
+ super(base);
+ }
+
+ public ApiEventType getEventType() {
+ return eventType;
+ }
+
+ public String getEventPlan() {
+ return eventPlan;
+ }
+
+ public String getEventPlanPhase() {
+ return eventPlanPhase;
+ }
+
+ public String getEventPriceList() {
+ return eventPriceList;
+ }
+
+ public boolean isFromDisk() {
+ return fromDisk;
+ }
+
+ public ApiEventBuilder setFromDisk(final boolean fromDisk) {
+ this.fromDisk = fromDisk;
+ return this;
+ }
+
+ public ApiEventBuilder setEventType(final ApiEventType eventType) {
+ this.eventType = eventType;
+ return this;
+ }
+
+ public ApiEventBuilder setEventPlan(final String eventPlan) {
+ this.eventPlan = eventPlan;
+ return this;
+ }
+
+ public ApiEventBuilder setEventPlanPhase(final String eventPlanPhase) {
+ this.eventPlanPhase = eventPlanPhase;
+ return this;
+ }
+
+ public ApiEventBuilder setEventPriceList(final String eventPriceList) {
+ this.eventPriceList = eventPriceList;
+ return this;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCancel.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCancel.java
new file mode 100644
index 0000000..5634f72
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCancel.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+public class ApiEventCancel extends ApiEventBase {
+
+
+ public ApiEventCancel(final ApiEventBuilder builder) {
+ super(builder.setEventType(ApiEventType.CANCEL));
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventChange.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventChange.java
new file mode 100644
index 0000000..dd440fc
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventChange.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+
+public class ApiEventChange extends ApiEventBase {
+
+ public ApiEventChange(final ApiEventBuilder builder) {
+ super(builder.setEventType(ApiEventType.CHANGE));
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCreate.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCreate.java
new file mode 100644
index 0000000..84d856e
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventCreate.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+
+public class ApiEventCreate extends ApiEventBase {
+
+ public ApiEventCreate(final ApiEventBuilder builder) {
+ super(builder.setEventType(ApiEventType.CREATE));
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateBilling.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateBilling.java
new file mode 100644
index 0000000..8cdd287
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateBilling.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+import org.joda.time.DateTime;
+
+public class ApiEventMigrateBilling extends ApiEventBase {
+ public ApiEventMigrateBilling(final ApiEventBuilder builder) {
+ super(builder.setEventType(ApiEventType.MIGRATE_BILLING));
+ }
+
+ public ApiEventMigrateBilling(final ApiEventMigrateSubscription input, final DateTime ctd) {
+ super(new ApiEventBuilder()
+ .setSubscriptionId(input.getSubscriptionId())
+ .setEventPlan(input.getEventPlan())
+ .setEventPlanPhase(input.getEventPlanPhase())
+ .setEventPriceList(input.getPriceList())
+ .setActiveVersion(input.getActiveVersion())
+ .setEffectiveDate(ctd)
+ .setProcessedDate(input.getProcessedDate())
+ .setRequestedDate(input.getRequestedDate())
+ .setFromDisk(true)
+ .setEventType(ApiEventType.MIGRATE_BILLING));
+ }
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateSubscription.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateSubscription.java
new file mode 100644
index 0000000..7594d91
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventMigrateSubscription.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+public class ApiEventMigrateSubscription extends ApiEventBase {
+
+ public ApiEventMigrateSubscription(final ApiEventBuilder builder) {
+ super(builder.setEventType(ApiEventType.MIGRATE_ENTITLEMENT));
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventReCreate.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventReCreate.java
new file mode 100644
index 0000000..2b1e025
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventReCreate.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+public class ApiEventReCreate extends ApiEventBase {
+
+ public ApiEventReCreate(final ApiEventBuilder builder) {
+ super(builder.setEventType(ApiEventType.RE_CREATE));
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventTransfer.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventTransfer.java
new file mode 100644
index 0000000..d432b0a
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventTransfer.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.subscription.events.user;
+
+public class ApiEventTransfer extends ApiEventBase {
+ public ApiEventTransfer(final ApiEventBuilder builder) {
+ super(builder.setEventType(ApiEventType.TRANSFER));
+ }
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventType.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventType.java
new file mode 100644
index 0000000..4006a7a
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventType.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+
+
+public enum ApiEventType {
+ MIGRATE_ENTITLEMENT {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return SubscriptionBaseTransitionType.MIGRATE_ENTITLEMENT;
+ }
+ },
+ CREATE {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return SubscriptionBaseTransitionType.CREATE;
+ }
+ },
+ MIGRATE_BILLING {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return SubscriptionBaseTransitionType.MIGRATE_BILLING;
+ }
+ },
+ TRANSFER {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return SubscriptionBaseTransitionType.TRANSFER;
+ }
+ },
+ CHANGE {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return SubscriptionBaseTransitionType.CHANGE;
+ }
+ },
+ RE_CREATE {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return SubscriptionBaseTransitionType.RE_CREATE;
+ }
+ },
+ CANCEL {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return SubscriptionBaseTransitionType.CANCEL;
+ }
+ },
+ UNCANCEL {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return SubscriptionBaseTransitionType.UNCANCEL;
+ }
+ };
+
+ // Used to map from internal events to User visible events (both user and phase)
+ public abstract SubscriptionBaseTransitionType getSubscriptionTransitionType();
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUncancel.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUncancel.java
new file mode 100644
index 0000000..5e56d95
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUncancel.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+public class ApiEventUncancel extends ApiEventBase {
+
+ public ApiEventUncancel(final ApiEventBuilder builder) {
+ super(builder.setEventType(ApiEventType.UNCANCEL));
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/exceptions/SubscriptionBaseError.java b/subscription/src/main/java/org/killbill/billing/subscription/exceptions/SubscriptionBaseError.java
new file mode 100644
index 0000000..3fa0f90
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/exceptions/SubscriptionBaseError.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.exceptions;
+
+public class SubscriptionBaseError extends Error {
+
+ private static final long serialVersionUID = 131398536;
+
+ public SubscriptionBaseError() {
+ super();
+ }
+
+ public SubscriptionBaseError(final String msg, final Throwable arg1) {
+ super(msg, arg1);
+ }
+
+ public SubscriptionBaseError(final String msg) {
+ super(msg);
+ }
+
+ public SubscriptionBaseError(final Throwable msg) {
+ super(msg);
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/glue/DefaultSubscriptionModule.java b/subscription/src/main/java/org/killbill/billing/subscription/glue/DefaultSubscriptionModule.java
new file mode 100644
index 0000000..7d53ac1
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/glue/DefaultSubscriptionModule.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.glue.SubscriptionModule;
+import org.killbill.billing.subscription.alignment.MigrationPlanAligner;
+import org.killbill.billing.subscription.alignment.PlanAligner;
+import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseService;
+import org.killbill.billing.subscription.api.migration.DefaultSubscriptionBaseMigrationApi;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi;
+import org.killbill.billing.subscription.api.svcs.DefaultSubscriptionInternalApi;
+import org.killbill.billing.subscription.api.timeline.DefaultSubscriptionBaseTimelineApi;
+import org.killbill.billing.subscription.api.timeline.RepairSubscriptionApiService;
+import org.killbill.billing.subscription.api.timeline.RepairSubscriptionLifecycleDao;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineApi;
+import org.killbill.billing.subscription.api.transfer.DefaultSubscriptionBaseTransferApi;
+import org.killbill.billing.subscription.api.transfer.SubscriptionBaseTransferApi;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseApiService;
+import org.killbill.billing.subscription.engine.addon.AddonUtils;
+import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService;
+import org.killbill.billing.subscription.engine.dao.DefaultSubscriptionDao;
+import org.killbill.billing.subscription.engine.dao.RepairSubscriptionDao;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.util.config.SubscriptionConfig;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+public class DefaultSubscriptionModule extends AbstractModule implements SubscriptionModule {
+
+ public static final String REPAIR_NAMED = "repair";
+
+ protected final ConfigSource configSource;
+
+ public DefaultSubscriptionModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ protected void installConfig() {
+ final SubscriptionConfig config = new ConfigurationObjectFactory(configSource).build(SubscriptionConfig.class);
+ bind(SubscriptionConfig.class).toInstance(config);
+ }
+
+ protected void installSubscriptionDao() {
+ bind(SubscriptionDao.class).to(DefaultSubscriptionDao.class).asEagerSingleton();
+ bind(SubscriptionDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class);
+ bind(RepairSubscriptionLifecycleDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class);
+ bind(RepairSubscriptionDao.class).asEagerSingleton();
+ }
+
+ protected void installSubscriptionCore() {
+ bind(SubscriptionBaseApiService.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionApiService.class).asEagerSingleton();
+ bind(SubscriptionBaseApiService.class).to(DefaultSubscriptionBaseApiService.class).asEagerSingleton();
+
+ bind(DefaultSubscriptionBaseService.class).asEagerSingleton();
+ bind(PlanAligner.class).asEagerSingleton();
+ bind(AddonUtils.class).asEagerSingleton();
+ bind(MigrationPlanAligner.class).asEagerSingleton();
+
+ installSubscriptionService();
+ installSubscriptionTimelineApi();
+ installSubscriptionMigrationApi();
+ installSubscriptionInternalApi();
+ installSubscriptionTransferApi();
+ }
+
+ @Override
+ protected void configure() {
+ installConfig();
+ installSubscriptionDao();
+ installSubscriptionCore();
+ }
+
+ @Override
+ public void installSubscriptionService() {
+ bind(SubscriptionBaseService.class).to(DefaultSubscriptionBaseService.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installSubscriptionTimelineApi() {
+ bind(SubscriptionBaseTimelineApi.class).to(DefaultSubscriptionBaseTimelineApi.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installSubscriptionMigrationApi() {
+ bind(SubscriptionBaseMigrationApi.class).to(DefaultSubscriptionBaseMigrationApi.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installSubscriptionInternalApi() {
+ bind(SubscriptionBaseInternalApi.class).to(DefaultSubscriptionInternalApi.class).asEagerSingleton();
+ }
+
+ @Override
+ public void installSubscriptionTransferApi() {
+ bind(SubscriptionBaseTransferApi.class).to(DefaultSubscriptionBaseTransferApi.class).asEagerSingleton();
+ }
+}
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql b/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
new file mode 100644
index 0000000..6d5b7c7
--- /dev/null
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
@@ -0,0 +1,72 @@
+/*! SET storage_engine=INNODB */;
+
+DROP TABLE IF EXISTS subscription_events;
+CREATE TABLE subscription_events (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ event_type varchar(9) NOT NULL,
+ user_type varchar(25) DEFAULT NULL,
+ requested_date datetime NOT NULL,
+ effective_date datetime NOT NULL,
+ subscription_id char(36) NOT NULL,
+ plan_name varchar(64) DEFAULT NULL,
+ phase_name varchar(128) DEFAULT NULL,
+ price_list_name varchar(64) DEFAULT NULL,
+ current_version int(11) DEFAULT 1,
+ is_active bool DEFAULT 1,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX subscription_events_id ON subscription_events(id);
+CREATE INDEX idx_ent_1 ON subscription_events(subscription_id, is_active, effective_date);
+CREATE INDEX idx_ent_2 ON subscription_events(subscription_id, effective_date, created_date, requested_date,id);
+CREATE INDEX subscription_events_tenant_account_record_id ON subscription_events(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS subscriptions;
+CREATE TABLE subscriptions (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ bundle_id char(36) NOT NULL,
+ category varchar(32) NOT NULL,
+ start_date datetime NOT NULL,
+ bundle_start_date datetime NOT NULL,
+ active_version int(11) DEFAULT 1,
+ charged_through_date datetime DEFAULT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX subscriptions_id ON subscriptions(id);
+CREATE INDEX subscriptions_bundle_id ON subscriptions(bundle_id);
+CREATE INDEX subscriptions_tenant_account_record_id ON subscriptions(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS bundles;
+CREATE TABLE bundles (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ external_key varchar(64) NOT NULL,
+ account_id char(36) NOT NULL,
+ last_sys_update_date datetime,
+ original_created_date datetime NOT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX bundles_id ON bundles(id);
+CREATE INDEX bundles_key ON bundles(external_key);
+CREATE INDEX bundles_account ON bundles(account_id);
+CREATE INDEX bundles_tenant_account_record_id ON bundles(tenant_record_id, account_record_id);
+
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg
new file mode 100644
index 0000000..c85a7f0
--- /dev/null
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/BundleSqlDao.sql.stg
@@ -0,0 +1,85 @@
+group BundleSqlDao: EntitySqlDao;
+
+tableName() ::= "bundles"
+
+
+tableFields(prefix) ::= <<
+ <prefix>external_key
+, <prefix>account_id
+, <prefix>last_sys_update_date
+, <prefix>original_created_date
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>updated_by
+, <prefix>updated_date
+>>
+
+tableValues() ::= <<
+ :externalKey
+, :accountId
+, :lastSysUpdateDate
+, :originalCreatedDate
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+updateBundleLastSysTime() ::= <<
+update <tableName()>
+set
+last_sys_update_date = :lastSysUpdateDate
+, updated_by = :createdBy
+, updated_date = :createdDate
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
+
+updateBundleExternalKey() ::= <<
+update <tableName()>
+set
+external_key = :externalKey
+, updated_by = :createdBy
+, updated_date = :createdDate
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
+
+getBundlesForKey() ::= <<
+select <allTableFields()>
+from bundles
+where
+external_key = :externalKey
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+getBundlesFromAccountAndKey() ::= <<
+select <allTableFields()>
+from bundles
+where
+external_key = :externalKey
+and account_id = :accountId
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+getBundleFromAccount() ::= <<
+select <allTableFields()>
+from bundles
+where
+account_id = :accountId
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+searchQuery(prefix) ::= <<
+ <idField(prefix)> = :searchKey
+ or <prefix>external_key = :searchKey
+ or <prefix>account_id = :searchKey
+>>
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
new file mode 100644
index 0000000..4550610
--- /dev/null
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionEventSqlDao.sql.stg
@@ -0,0 +1,113 @@
+group EventSqlDao: EntitySqlDao;
+
+tableName() ::= "subscription_events"
+
+andCheckSoftDeletionWithComma(prefix) ::= "and <prefix>is_active"
+
+extraTableFieldsWithComma(prefix) ::= <<
+, <prefix>record_id as total_ordering
+>>
+
+defaultOrderBy(prefix) ::= <<
+order by <prefix>effective_date ASC, <recordIdField(prefix)> ASC
+>>
+
+
+tableFields(prefix) ::= <<
+ <prefix> event_type
+, <prefix> user_type
+, <prefix> requested_date
+, <prefix> effective_date
+, <prefix> subscription_id
+, <prefix> plan_name
+, <prefix> phase_name
+, <prefix> price_list_name
+, <prefix> current_version
+, <prefix> is_active
+, <prefix> created_by
+, <prefix> created_date
+, <prefix> updated_by
+, <prefix> updated_date
+>>
+
+tableValues() ::= <<
+ :eventType
+, :userType
+, :requestedDate
+, :effectiveDate
+, :subscriptionId
+, :planName
+, :phaseName
+, :priceListName
+, :currentVersion
+, :isActive
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+
+updateVersion() ::= <<
+update <tableName()>
+set
+current_version = :currentVersion
+, updated_by = :createdBy
+, updated_date = :createdDate
+where
+id = :id
+<AND_CHECK_TENANT()>
+;
+>>
+
+unactiveEvent() ::= <<
+update <tableName()>
+set
+is_active = 0
+, updated_by = :createdBy
+, updated_date = :createdDate
+where
+id = :id
+<AND_CHECK_TENANT()>
+;
+>>
+
+reactiveEvent() ::= <<
+update <tableName()>
+set
+is_active = 1
+, updated_by = :createdBy
+, updated_date = :createdDate
+where
+event_id = :eventId
+<AND_CHECK_TENANT()>
+;
+>>
+
+
+
+getFutureActiveEventForSubscription() ::= <<
+select <allTableFields()>
+, record_id as total_ordering
+from <tableName()>
+where
+subscription_id = :subscriptionId
+and is_active = 1
+and effective_date > :now
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+getEventsForSubscription() ::= <<
+select <allTableFields()>
+, record_id as total_ordering
+from <tableName()>
+where
+subscription_id = :subscriptionId
+and is_active = 1
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionSqlDao.sql.stg b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionSqlDao.sql.stg
new file mode 100644
index 0000000..55b2cdb
--- /dev/null
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/engine/dao/SubscriptionSqlDao.sql.stg
@@ -0,0 +1,74 @@
+group SubscriptionSqlDao: EntitySqlDao;
+
+tableName() ::= "subscriptions"
+
+tableFields(prefix) ::= <<
+ <prefix>bundle_id
+, <prefix>category
+, <prefix>start_date
+, <prefix>bundle_start_date
+, <prefix>active_version
+, <prefix>charged_through_date
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>updated_by
+, <prefix>updated_date
+>>
+
+tableValues() ::= <<
+ :bundleId
+, :category
+, :startDate
+, :bundleStartDate
+, :activeVersion
+, :chargedThroughDate
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+
+getSubscriptionsFromBundleId() ::= <<
+select
+<allTableFields()>
+from <tableName()>
+where bundle_id = :bundleId
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+updateChargedThroughDate() ::= <<
+update <tableName()>
+set
+charged_through_date = :chargedThroughDate
+, updated_by = :createdBy
+, updated_date = :createdDate
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
+
+updateActiveVersion() ::= <<
+update <tableName()>
+set
+active_version = :activeVersion
+, updated_by = :createdBy
+, updated_date = :createdDate
+where id = :id
+;
+>>
+
+updateForRepair() ::= <<
+update <tableName()>
+set
+active_version = :activeVersion
+, start_date = :startDate
+, bundle_start_date = :bundleStartDate
+, updated_by = :createdBy
+, updated_date = :createdDate
+where id = :id
+<AND_CHECK_TENANT()>
+;
+>>
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java b/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java
new file mode 100644
index 0000000..927afd9
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestPlanAligner.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.alignment;
+
+import java.util.List;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.DefaultCatalogService;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.io.VersionedCatalogLoader;
+import org.killbill.clock.DefaultClock;
+import org.killbill.billing.subscription.SubscriptionTestSuiteNoDB;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.user.ApiEventBase;
+import org.killbill.billing.subscription.events.user.ApiEventBuilder;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.config.CatalogConfig;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class TestPlanAligner extends SubscriptionTestSuiteNoDB {
+
+ private static final String priceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ private final DefaultClock clock = new DefaultClock();
+
+ private DefaultCatalogService catalogService;
+ private PlanAligner planAligner;
+
+ @Override
+ @BeforeClass(groups = "fast")
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ final VersionedCatalogLoader versionedCatalogLoader = new VersionedCatalogLoader(clock);
+ final CatalogConfig config = new ConfigurationObjectFactory(new ConfigSource() {
+ final Map<String, String> properties = ImmutableMap.<String, String>of("org.killbill.catalog.uri", "file:src/test/resources/testInput.xml");
+
+ @Override
+ public String getString(final String propertyName) {
+ return properties.get(propertyName);
+ }
+ }).build(CatalogConfig.class);
+
+ catalogService = new DefaultCatalogService(config, versionedCatalogLoader);
+ planAligner = new PlanAligner(catalogService);
+
+ catalogService.loadCatalog();
+ }
+
+ @Test(groups = "fast")
+ public void testCreationBundleAlignment() throws Exception {
+ final String productName = "pistol-monthly";
+ final PhaseType initialPhase = PhaseType.TRIAL;
+ final DefaultSubscriptionBase defaultSubscriptionBase = createSubscriptionStartedInThePast(productName, initialPhase);
+
+ // Make the creation effective now, after the bundle and the subscription started
+ final DateTime effectiveDate = clock.getUTCNow();
+ final TimedPhase[] phases = getTimedPhasesOnCreate(productName, initialPhase, defaultSubscriptionBase, effectiveDate);
+
+ // All plans but Laser-Scope are START_OF_BUNDLE aligned on creation
+ Assert.assertEquals(phases[0].getStartPhase(), defaultSubscriptionBase.getBundleStartDate());
+ Assert.assertEquals(phases[1].getStartPhase(), defaultSubscriptionBase.getBundleStartDate().plusDays(30));
+
+ // Verify the next phase via the other API
+ final TimedPhase nextTimePhase = planAligner.getNextTimedPhase(defaultSubscriptionBase, effectiveDate, effectiveDate);
+ Assert.assertEquals(nextTimePhase.getStartPhase(), defaultSubscriptionBase.getBundleStartDate().plusDays(30));
+
+ // Now look at the past, before the bundle started
+ final DateTime effectiveDateInThePast = defaultSubscriptionBase.getBundleStartDate().minusHours(10);
+ final TimedPhase[] phasesInThePast = getTimedPhasesOnCreate(productName, initialPhase, defaultSubscriptionBase, effectiveDateInThePast);
+ Assert.assertNull(phasesInThePast[0]);
+ Assert.assertEquals(phasesInThePast[1].getStartPhase(), defaultSubscriptionBase.getBundleStartDate());
+
+ // Verify the next phase via the other API
+ try {
+ planAligner.getNextTimedPhase(defaultSubscriptionBase, effectiveDateInThePast, effectiveDateInThePast);
+ Assert.fail("Can't use getNextTimedPhase(): the effective date is before the initial plan");
+ } catch (SubscriptionBaseError e) {
+ Assert.assertTrue(true);
+ }
+
+ // Try a change plan now (simulate an IMMEDIATE policy)
+ final String newProductName = "shotgun-monthly";
+ final DateTime effectiveChangeDate = clock.getUTCNow();
+ changeSubscription(effectiveChangeDate, defaultSubscriptionBase, productName, newProductName, initialPhase);
+
+ // All non rescue plans are START_OF_SUBSCRIPTION aligned on change
+ final TimedPhase newPhase = getNextTimedPhaseOnChange(defaultSubscriptionBase, newProductName, effectiveChangeDate);
+ Assert.assertEquals(newPhase.getStartPhase(), defaultSubscriptionBase.getStartDate().plusDays(30),
+ String.format("Start phase: %s, but bundle start date: %s and subscription start date: %s",
+ newPhase.getStartPhase(), defaultSubscriptionBase.getBundleStartDate(), defaultSubscriptionBase.getStartDate()));
+ }
+
+ @Test(groups = "fast")
+ public void testCreationSubscriptionAlignment() throws Exception {
+ final String productName = "laser-scope-monthly";
+ final PhaseType initialPhase = PhaseType.DISCOUNT;
+ final DefaultSubscriptionBase defaultSubscriptionBase = createSubscriptionStartedInThePast(productName, initialPhase);
+
+ // Look now, after the bundle and the subscription started
+ final DateTime effectiveDate = clock.getUTCNow();
+ final TimedPhase[] phases = getTimedPhasesOnCreate(productName, initialPhase, defaultSubscriptionBase, effectiveDate);
+
+ // Laser-Scope is START_OF_SUBSCRIPTION aligned on creation
+ Assert.assertEquals(phases[0].getStartPhase(), defaultSubscriptionBase.getStartDate());
+ Assert.assertEquals(phases[1].getStartPhase(), defaultSubscriptionBase.getStartDate().plusMonths(1));
+
+ // Verify the next phase via the other API
+ final TimedPhase nextTimePhase = planAligner.getNextTimedPhase(defaultSubscriptionBase, effectiveDate, effectiveDate);
+ Assert.assertEquals(nextTimePhase.getStartPhase(), defaultSubscriptionBase.getStartDate().plusMonths(1));
+
+ // Now look at the past, before the subscription started
+ final DateTime effectiveDateInThePast = defaultSubscriptionBase.getStartDate().minusHours(10);
+ final TimedPhase[] phasesInThePast = getTimedPhasesOnCreate(productName, initialPhase, defaultSubscriptionBase, effectiveDateInThePast);
+ Assert.assertNull(phasesInThePast[0]);
+ Assert.assertEquals(phasesInThePast[1].getStartPhase(), defaultSubscriptionBase.getStartDate());
+
+ // Verify the next phase via the other API
+ try {
+ planAligner.getNextTimedPhase(defaultSubscriptionBase, effectiveDateInThePast, effectiveDateInThePast);
+ Assert.fail("Can't use getNextTimedPhase(): the effective date is before the initial plan");
+ } catch (SubscriptionBaseError e) {
+ Assert.assertTrue(true);
+ }
+
+ // Try a change plan (simulate END_OF_TERM policy)
+ final String newProductName = "telescopic-scope-monthly";
+ final DateTime effectiveChangeDate = defaultSubscriptionBase.getStartDate().plusMonths(1);
+ changeSubscription(effectiveChangeDate, defaultSubscriptionBase, productName, newProductName, initialPhase);
+
+ // All non rescue plans are START_OF_SUBSCRIPTION aligned on change. Since we're END_OF_TERM here, we'll
+ // never see the discount phase of telescopic-scope-monthly and jump right into evergreen.
+ // But in this test, since we didn't create the future change event from discount to evergreen (see changeSubscription,
+ // the subscription has only two transitions), we'll see null
+ final TimedPhase newPhase = getNextTimedPhaseOnChange(defaultSubscriptionBase, newProductName, effectiveChangeDate);
+ Assert.assertNull(newPhase);
+ }
+
+ private DefaultSubscriptionBase createSubscriptionStartedInThePast(final String productName, final PhaseType phaseType) {
+ final SubscriptionBuilder builder = new SubscriptionBuilder();
+ builder.setBundleStartDate(clock.getUTCNow().minusHours(10));
+ // Make sure to set the dates apart
+ builder.setAlignStartDate(new DateTime(builder.getBundleStartDate().plusHours(5)));
+
+ // Create the transitions
+ final DefaultSubscriptionBase defaultSubscriptionBase = new DefaultSubscriptionBase(builder, null, clock);
+ final SubscriptionBaseEvent event = createSubscriptionEvent(builder.getAlignStartDate(),
+ productName,
+ phaseType,
+ ApiEventType.CREATE,
+ defaultSubscriptionBase.getActiveVersion());
+ defaultSubscriptionBase.rebuildTransitions(ImmutableList.<SubscriptionBaseEvent>of(event), catalogService.getFullCatalog());
+
+ Assert.assertEquals(defaultSubscriptionBase.getAllTransitions().size(), 1);
+ Assert.assertNull(defaultSubscriptionBase.getAllTransitions().get(0).getPreviousPhase());
+ Assert.assertNotNull(defaultSubscriptionBase.getAllTransitions().get(0).getNextPhase());
+
+ return defaultSubscriptionBase;
+ }
+
+ private void changeSubscription(final DateTime effectiveChangeDate,
+ final DefaultSubscriptionBase defaultSubscriptionBase,
+ final String previousProductName,
+ final String newProductName,
+ final PhaseType commonPhaseType) {
+ final SubscriptionBaseEvent previousEvent = createSubscriptionEvent(defaultSubscriptionBase.getStartDate(),
+ previousProductName,
+ commonPhaseType,
+ ApiEventType.CREATE,
+ defaultSubscriptionBase.getActiveVersion());
+ final SubscriptionBaseEvent event = createSubscriptionEvent(effectiveChangeDate,
+ newProductName,
+ commonPhaseType,
+ ApiEventType.CHANGE,
+ defaultSubscriptionBase.getActiveVersion());
+
+ defaultSubscriptionBase.rebuildTransitions(ImmutableList.<SubscriptionBaseEvent>of(previousEvent, event), catalogService.getFullCatalog());
+
+ final List<SubscriptionBaseTransition> newTransitions = defaultSubscriptionBase.getAllTransitions();
+ Assert.assertEquals(newTransitions.size(), 2);
+ Assert.assertNull(newTransitions.get(0).getPreviousPhase());
+ Assert.assertEquals(newTransitions.get(0).getNextPhase(), newTransitions.get(1).getPreviousPhase());
+ Assert.assertNotNull(newTransitions.get(1).getNextPhase());
+ }
+
+ private SubscriptionBaseEvent createSubscriptionEvent(final DateTime effectiveDate,
+ final String productName,
+ final PhaseType phaseType,
+ final ApiEventType apiEventType,
+ final long activeVersion) {
+ final ApiEventBuilder eventBuilder = new ApiEventBuilder();
+ eventBuilder.setEffectiveDate(effectiveDate);
+ eventBuilder.setEventPlan(productName);
+ eventBuilder.setEventPlanPhase(productName + "-" + phaseType.toString().toLowerCase());
+ eventBuilder.setEventPriceList(priceList);
+
+ // We don't really use the following but the code path requires it
+ eventBuilder.setRequestedDate(effectiveDate);
+ eventBuilder.setFromDisk(true);
+ eventBuilder.setActiveVersion(activeVersion);
+
+ return new ApiEventBase(eventBuilder.setEventType(apiEventType));
+ }
+
+ private TimedPhase getNextTimedPhaseOnChange(final DefaultSubscriptionBase defaultSubscriptionBase,
+ final String newProductName,
+ final DateTime effectiveChangeDate) throws CatalogApiException, SubscriptionBaseApiException {
+ // The date is used for different catalog versions - we don't care here
+ final Plan newPlan = catalogService.getFullCatalog().findPlan(newProductName, clock.getUTCNow());
+
+ return planAligner.getNextTimedPhaseOnChange(defaultSubscriptionBase, newPlan, priceList, effectiveChangeDate, effectiveChangeDate);
+ }
+
+ private TimedPhase[] getTimedPhasesOnCreate(final String productName,
+ final PhaseType initialPhase,
+ final DefaultSubscriptionBase defaultSubscriptionBase,
+ final DateTime effectiveDate) throws CatalogApiException, SubscriptionBaseApiException {
+ // The date is used for different catalog versions - we don't care here
+ final Plan plan = catalogService.getFullCatalog().findPlan(productName, clock.getUTCNow());
+
+ // Same here for the requested date
+ final TimedPhase[] phases = planAligner.getCurrentAndNextTimedPhaseOnCreate(defaultSubscriptionBase, plan, initialPhase, priceList, clock.getUTCNow(), effectiveDate);
+ Assert.assertEquals(phases.length, 2);
+
+ return phases;
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestTimedMigration.java b/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestTimedMigration.java
new file mode 100644
index 0000000..b312873
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestTimedMigration.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.alignment;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.subscription.SubscriptionTestSuiteNoDB;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+
+public class TestTimedMigration extends SubscriptionTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testConstructor() throws Exception {
+ final DateTime eventTime = new DateTime(DateTimeZone.UTC);
+ final SubscriptionBaseEvent.EventType eventType = SubscriptionBaseEvent.EventType.API_USER;
+ final ApiEventType apiEventType = ApiEventType.CREATE;
+ final Plan plan = Mockito.mock(Plan.class);
+ final PlanPhase phase = Mockito.mock(PlanPhase.class);
+ final String priceList = UUID.randomUUID().toString();
+ final TimedMigration timedMigration = new TimedMigration(eventTime, eventType, apiEventType, plan, phase, priceList);
+ final TimedMigration otherTimedMigration = new TimedMigration(eventTime, eventType, apiEventType, plan, phase, priceList);
+
+ Assert.assertEquals(otherTimedMigration, timedMigration);
+ Assert.assertEquals(timedMigration.getEventTime(), eventTime);
+ Assert.assertEquals(timedMigration.getEventType(), eventType);
+ Assert.assertEquals(timedMigration.getApiEventType(), apiEventType);
+ Assert.assertEquals(timedMigration.getPlan(), plan);
+ Assert.assertEquals(timedMigration.getPhase(), phase);
+ Assert.assertEquals(timedMigration.getPriceList(), priceList);
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestTimedPhase.java b/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestTimedPhase.java
new file mode 100644
index 0000000..ffa07aa
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/alignment/TestTimedPhase.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.alignment;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.subscription.SubscriptionTestSuiteNoDB;
+
+public class TestTimedPhase extends SubscriptionTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testConstructor() throws Exception {
+ final PlanPhase planPhase = Mockito.mock(PlanPhase.class);
+ final DateTime startPhase = new DateTime(DateTimeZone.UTC);
+ final TimedPhase timedPhase = new TimedPhase(planPhase, startPhase);
+ final TimedPhase otherTimedPhase = new TimedPhase(planPhase, startPhase);
+
+ Assert.assertEquals(otherTimedPhase, timedPhase);
+ Assert.assertEquals(timedPhase.getPhase(), planPhase);
+ Assert.assertEquals(timedPhase.getStartPhase(), startPhase);
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/migration/TestMigration.java b/subscription/src/test/java/org/killbill/billing/subscription/api/migration/TestMigration.java
new file mode 100644
index 0000000..22e1e04
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/migration/TestMigration.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.migration;
+
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.AccountMigration;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestMigration extends SubscriptionTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testSingleBasePlan() throws SubscriptionBaseMigrationApiException {
+ final DateTime startDate = clock.getUTCNow().minusMonths(2);
+ final DateTime beforeMigration = clock.getUTCNow();
+ final AccountMigration toBeMigrated = testUtil.createAccountForMigrationWithRegularBasePlan(startDate);
+ final DateTime afterMigration = clock.getUTCNow();
+
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+ migrationApi.migrate(toBeMigrated, callContext);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseBundle> bundles = subscriptionInternalApi.getBundlesForAccount(toBeMigrated.getAccountKey(), internalCallContext);
+ assertEquals(bundles.size(), 1);
+ final SubscriptionBaseBundle bundle = bundles.get(0);
+
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+ final SubscriptionBase subscription = subscriptions.get(0);
+ assertTrue(subscription.getStartDate().compareTo(startDate) == 0);
+ assertEquals(subscription.getEndDate(), null);
+ assertEquals(subscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(subscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(subscription.getCurrentPlan().getName(), "shotgun-annual");
+ assertEquals(subscription.getChargedThroughDate(), startDate.plusYears(1));
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testPlanWithAddOn() throws SubscriptionBaseMigrationApiException {
+ final DateTime beforeMigration = clock.getUTCNow();
+ final DateTime initalBPStart = clock.getUTCNow().minusMonths(3);
+ final DateTime initalAddonStart = clock.getUTCNow().minusMonths(1).plusDays(7);
+ final AccountMigration toBeMigrated = testUtil.createAccountForMigrationWithRegularBasePlanAndAddons(initalBPStart, initalAddonStart);
+ final DateTime afterMigration = clock.getUTCNow();
+
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+ migrationApi.migrate(toBeMigrated, callContext);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseBundle> bundles = subscriptionInternalApi.getBundlesForAccount(toBeMigrated.getAccountKey(), internalCallContext);
+ assertEquals(bundles.size(), 1);
+ final SubscriptionBaseBundle bundle = bundles.get(0);
+
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 2);
+
+ final SubscriptionBase baseSubscription = (subscriptions.get(0).getCurrentPlan().getProduct().getCategory() == ProductCategory.BASE) ?
+ subscriptions.get(0) : subscriptions.get(1);
+ assertTrue(baseSubscription.getStartDate().compareTo(initalBPStart) == 0);
+ assertEquals(baseSubscription.getEndDate(), null);
+ assertEquals(baseSubscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(baseSubscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(baseSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(baseSubscription.getCurrentPlan().getName(), "shotgun-annual");
+ assertEquals(baseSubscription.getChargedThroughDate(), initalBPStart.plusYears(1));
+
+ final SubscriptionBase aoSubscription = (subscriptions.get(0).getCurrentPlan().getProduct().getCategory() == ProductCategory.ADD_ON) ?
+ subscriptions.get(0) : subscriptions.get(1);
+ // initalAddonStart.plusMonths(1).minusMonths(1) may be different from initalAddonStart, depending on exact date
+ // e.g : March 31 + 1 month => April 30 and April 30 - 1 month = March 30 which is != March 31 !!!!
+ assertEquals(aoSubscription.getStartDate(), initalAddonStart.plusMonths(1).minusMonths(1));
+ assertEquals(aoSubscription.getEndDate(), null);
+ assertEquals(aoSubscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(aoSubscription.getCurrentPhase().getPhaseType(), PhaseType.DISCOUNT);
+ assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(aoSubscription.getCurrentPlan().getName(), "telescopic-scope-monthly");
+ assertEquals(aoSubscription.getChargedThroughDate(), initalAddonStart.plusMonths(1));
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testSingleBasePlanFutureCancelled() throws SubscriptionBaseMigrationApiException {
+ final DateTime startDate = clock.getUTCNow().minusMonths(1);
+ final DateTime beforeMigration = clock.getUTCNow();
+ final AccountMigration toBeMigrated = testUtil.createAccountForMigrationWithRegularBasePlanFutreCancelled(startDate);
+ final DateTime afterMigration = clock.getUTCNow();
+
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+ migrationApi.migrate(toBeMigrated, callContext);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseBundle> bundles = subscriptionInternalApi.getBundlesForAccount(toBeMigrated.getAccountKey(), internalCallContext);
+ assertEquals(bundles.size(), 1);
+ final SubscriptionBaseBundle bundle = bundles.get(0);
+ //assertEquals(bundle.getStartDate(), effectiveDate);
+
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+ final SubscriptionBase subscription = subscriptions.get(0);
+ assertTrue(subscription.getStartDate().compareTo(startDate) == 0);
+ assertEquals(subscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(subscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-annual");
+ assertEquals(subscription.getChargedThroughDate(), startDate.plusYears(1));
+
+ // The MIGRATE_BILLING will not be there because the subscription is cancelled at the same date so no BILLING should occur
+ //testListener.pushExpectedEvent(NextEvent.MIGRATE_BILLING);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusYears(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ assertTrue(subscription.getStartDate().compareTo(startDate) == 0);
+ assertNotNull(subscription.getEndDate());
+ assertEquals(subscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(subscription.getCurrentPhase(), null);
+ assertEquals(subscription.getState(), EntitlementState.CANCELLED);
+ assertNull(subscription.getCurrentPlan());
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testSingleBasePlanWithPendingPhase() throws SubscriptionBaseMigrationApiException {
+ final DateTime trialDate = clock.getUTCNow().minusDays(10);
+ final AccountMigration toBeMigrated = testUtil.createAccountForMigrationFuturePendingPhase(trialDate);
+
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+ migrationApi.migrate(toBeMigrated, callContext);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseBundle> bundles = subscriptionInternalApi.getBundlesForAccount(toBeMigrated.getAccountKey(), internalCallContext);
+ assertEquals(bundles.size(), 1);
+ final SubscriptionBaseBundle bundle = bundles.get(0);
+
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+ final SubscriptionBase subscription = subscriptions.get(0);
+
+ assertEquals(subscription.getStartDate(), trialDate);
+ assertEquals(subscription.getEndDate(), null);
+ assertEquals(subscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.TRIAL);
+ assertEquals(subscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-monthly");
+ assertEquals(subscription.getChargedThroughDate(), trialDate.plusDays(30));
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_BILLING);
+
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(30));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ assertEquals(subscription.getStartDate(), trialDate);
+ assertEquals(subscription.getEndDate(), null);
+ assertEquals(subscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(subscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-monthly");
+ assertEquals(subscription.getCurrentPhase().getName(), "assault-rifle-monthly-evergreen");
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testSingleBasePlanWithPendingChange() throws SubscriptionBaseMigrationApiException {
+ final DateTime beforeMigration = clock.getUTCNow();
+ final AccountMigration toBeMigrated = testUtil.createAccountForMigrationFuturePendingChange();
+ final DateTime afterMigration = clock.getUTCNow();
+
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+ migrationApi.migrate(toBeMigrated, callContext);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseBundle> bundles = subscriptionInternalApi.getBundlesForAccount(toBeMigrated.getAccountKey(), internalCallContext);
+ assertEquals(bundles.size(), 1);
+ final SubscriptionBaseBundle bundle = bundles.get(0);
+
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+ final SubscriptionBase subscription = subscriptions.get(0);
+ //assertDateWithin(subscription.getStartDate(), beforeMigration, afterMigration);
+ assertEquals(subscription.getEndDate(), null);
+ assertEquals(subscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(subscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-monthly");
+
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_BILLING);
+
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ //assertDateWithin(subscription.getStartDate(), beforeMigration, afterMigration);
+ assertEquals(subscription.getEndDate(), null);
+ assertEquals(subscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(subscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(subscription.getCurrentPlan().getName(), "shotgun-annual");
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testChangePriorMigrateBilling() throws Exception {
+ final DateTime startDate = clock.getUTCNow().minusMonths(2);
+ final DateTime beforeMigration = clock.getUTCNow();
+ final AccountMigration toBeMigrated = testUtil.createAccountForMigrationWithRegularBasePlan(startDate);
+ final DateTime afterMigration = clock.getUTCNow();
+
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+ migrationApi.migrate(toBeMigrated, callContext);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseBundle> bundles = subscriptionInternalApi.getBundlesForAccount(toBeMigrated.getAccountKey(), internalCallContext);
+ assertEquals(bundles.size(), 1);
+
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundles.get(0).getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptions.get(0);
+
+ final List<SubscriptionBaseTransition> transitions = subscription.getAllTransitions();
+ assertEquals(transitions.size(), 2);
+ final SubscriptionBaseTransitionData initialMigrateBilling = (SubscriptionBaseTransitionData) transitions.get(1);
+ assertEquals(initialMigrateBilling.getApiEventType(), ApiEventType.MIGRATE_BILLING);
+ assertTrue(initialMigrateBilling.getEffectiveTransitionTime().compareTo(subscription.getChargedThroughDate()) == 0);
+ assertEquals(initialMigrateBilling.getNextPlan().getName(), "shotgun-annual");
+ assertEquals(initialMigrateBilling.getNextPhase().getName(), "shotgun-annual-evergreen");
+
+ final List<SubscriptionBaseTransition> billingTransitions = subscription.getBillingTransitions();
+ assertEquals(billingTransitions.size(), 1);
+ assertEquals(billingTransitions.get(0), initialMigrateBilling);
+
+ // Now make an IMMEDIATE change of plan
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ subscription.changePlanWithDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, clock.getUTCNow(), callContext);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseTransition> newTransitions = subscription.getAllTransitions();
+ assertEquals(newTransitions.size(), 3);
+
+ final SubscriptionBaseTransitionData changeTransition = (SubscriptionBaseTransitionData) newTransitions.get(1);
+ assertEquals(changeTransition.getApiEventType(), ApiEventType.CHANGE);
+
+ final SubscriptionBaseTransitionData newMigrateBilling = (SubscriptionBaseTransitionData) newTransitions.get(2);
+ assertEquals(newMigrateBilling.getApiEventType(), ApiEventType.MIGRATE_BILLING);
+ assertTrue(newMigrateBilling.getEffectiveTransitionTime().compareTo(subscription.getChargedThroughDate()) == 0);
+ assertTrue(newMigrateBilling.getEffectiveTransitionTime().compareTo(initialMigrateBilling.getEffectiveTransitionTime()) == 0);
+ assertEquals(newMigrateBilling.getNextPlan().getName(), "assault-rifle-monthly");
+ assertEquals(newMigrateBilling.getNextPhase().getName(), "assault-rifle-monthly-evergreen");
+
+ final List<SubscriptionBaseTransition> newBillingTransitions = subscription.getBillingTransitions();
+ assertEquals(newBillingTransitions.size(), 1);
+ assertEquals(newBillingTransitions.get(0), newMigrateBilling);
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java b/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java
new file mode 100644
index 0000000..2121dc0
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/TestEventJson.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.timeline.DefaultRepairSubscriptionEvent;
+import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.events.RepairSubscriptionInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+public class TestEventJson extends GuicyKillbillTestSuiteNoDB {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Test(groups = "fast")
+ public void testSubscriptionEvent() throws Exception {
+
+ final EffectiveSubscriptionInternalEvent e = new DefaultEffectiveSubscriptionEvent(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), new DateTime(), new DateTime(),
+ EntitlementState.ACTIVE, "pro", "TRIAL", "DEFAULT", EntitlementState.CANCELLED, null, null, null, 3L,
+ SubscriptionBaseTransitionType.CANCEL, 0, new DateTime(), 1L, 2L, null);
+
+ final String json = mapper.writeValueAsString(e);
+
+ final Class<?> claz = Class.forName(DefaultEffectiveSubscriptionEvent.class.getName());
+ final Object obj = mapper.readValue(json, claz);
+ Assert.assertTrue(obj.equals(e));
+ }
+
+ @Test(groups = "fast")
+ public void testRepairSubscriptionEvent() throws Exception {
+ final RepairSubscriptionInternalEvent e = new DefaultRepairSubscriptionEvent(UUID.randomUUID(), UUID.randomUUID(), new DateTime(), 1L, 2L, null);
+
+ final String json = mapper.writeValueAsString(e);
+
+ final Class<?> claz = Class.forName(DefaultRepairSubscriptionEvent.class.getName());
+ final Object obj = mapper.readValue(json, claz);
+ Assert.assertTrue(obj.equals(e));
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairBP.java b/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairBP.java
new file mode 100644
index 0000000..6473cd9
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairBP.java
@@ -0,0 +1,702 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.DeletedEvent;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.ExistingEvent;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.NewEvent;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.user.SubscriptionEvents;
+import org.killbill.billing.subscription.api.user.TestSubscriptionHelper.TestWithException;
+import org.killbill.billing.subscription.api.user.TestSubscriptionHelper.TestWithExceptionCallback;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestRepairBP extends SubscriptionTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testFetchBundleRepair() throws Exception {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ final String aoProduct = "Telescopic-Scope";
+ final BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList);
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ final List<SubscriptionBaseTimeline> subscriptionRepair = bundleRepair.getSubscriptions();
+ assertEquals(subscriptionRepair.size(), 2);
+
+ for (final SubscriptionBaseTimeline cur : subscriptionRepair) {
+ assertNull(cur.getDeletedEvents());
+ assertNull(cur.getNewEvents());
+
+ final List<ExistingEvent> events = cur.getExistingEvents();
+ assertEquals(events.size(), 2);
+ testUtil.sortExistingEvent(events);
+
+ assertEquals(events.get(0).getSubscriptionTransitionType(), SubscriptionBaseTransitionType.CREATE);
+ assertEquals(events.get(1).getSubscriptionTransitionType(), SubscriptionBaseTransitionType.PHASE);
+ final boolean isBP = cur.getId().equals(baseSubscription.getId());
+ if (isBP) {
+ assertEquals(cur.getId(), baseSubscription.getId());
+
+ assertEquals(events.get(0).getPlanPhaseSpecifier().getProductName(), baseProduct);
+ assertEquals(events.get(0).getPlanPhaseSpecifier().getPhaseType(), PhaseType.TRIAL);
+ assertEquals(events.get(0).getPlanPhaseSpecifier().getProductCategory(), ProductCategory.BASE);
+ assertEquals(events.get(0).getPlanPhaseSpecifier().getPriceListName(), basePriceList);
+ assertEquals(events.get(0).getPlanPhaseSpecifier().getBillingPeriod(), BillingPeriod.NO_BILLING_PERIOD);
+
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getProductName(), baseProduct);
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getProductCategory(), ProductCategory.BASE);
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getPriceListName(), basePriceList);
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getBillingPeriod(), baseTerm);
+ } else {
+ assertEquals(cur.getId(), aoSubscription.getId());
+
+ assertEquals(events.get(0).getPlanPhaseSpecifier().getProductName(), aoProduct);
+ assertEquals(events.get(0).getPlanPhaseSpecifier().getPhaseType(), PhaseType.DISCOUNT);
+ assertEquals(events.get(0).getPlanPhaseSpecifier().getProductCategory(), ProductCategory.ADD_ON);
+ assertEquals(events.get(0).getPlanPhaseSpecifier().getPriceListName(), aoPriceList);
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getBillingPeriod(), aoTerm);
+
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getProductName(), aoProduct);
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getProductCategory(), ProductCategory.ADD_ON);
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getPriceListName(), aoPriceList);
+ assertEquals(events.get(1).getPlanPhaseSpecifier().getBillingPeriod(), aoTerm);
+ }
+ }
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testBPRepairWithCancellationOnstart() throws Exception {
+ final String baseProduct = "Shotgun";
+ final DateTime startDate = clock.getUTCNow();
+
+ // CREATE BP
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate);
+
+ // Stays in trial-- for instance
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(10));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId()));
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CANCEL, baseSubscription.getStartDate(), null);
+
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ // FIRST ISSUE DRY RUN
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ boolean dryRun = true;
+ final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+ testUtil.sortEventsOnBundle(dryRunBundleRepair);
+ List<SubscriptionBaseTimeline> subscriptionRepair = dryRunBundleRepair.getSubscriptions();
+ assertEquals(subscriptionRepair.size(), 1);
+ SubscriptionBaseTimeline cur = subscriptionRepair.get(0);
+ int index = 0;
+ final List<ExistingEvent> events = subscriptionRepair.get(0).getExistingEvents();
+ assertEquals(events.size(), 2);
+ final List<ExistingEvent> expected = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, baseProduct, PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, baseSubscription.getStartDate()));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, baseProduct, PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, baseSubscription.getStartDate()));
+
+ for (final ExistingEvent e : expected) {
+ testUtil.validateExistingEventForAssertion(e, events.get(index++));
+ }
+
+ final DefaultSubscriptionBase dryRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ assertEquals(dryRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+ assertEquals(dryRunBaseSubscription.getBundleId(), bundle.getId());
+ assertEquals(dryRunBaseSubscription.getStartDate(), baseSubscription.getStartDate());
+
+ final Plan currentPlan = dryRunBaseSubscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), baseProduct);
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ final PlanPhase currentPhase = dryRunBaseSubscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // SECOND RE-ISSUE CALL-- NON DRY RUN
+ dryRun = false;
+ testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
+ final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+ assertListenerStatus();
+
+ subscriptionRepair = realRunBundleRepair.getSubscriptions();
+ assertEquals(subscriptionRepair.size(), 1);
+ cur = subscriptionRepair.get(0);
+ assertEquals(cur.getId(), baseSubscription.getId());
+ index = 0;
+ for (final ExistingEvent e : expected) {
+ testUtil.validateExistingEventForAssertion(e, events.get(index++));
+ }
+ final DefaultSubscriptionBase realRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(realRunBaseSubscription.getAllTransitions().size(), 2);
+
+ assertEquals(realRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+ assertEquals(realRunBaseSubscription.getBundleId(), bundle.getId());
+ assertEquals(realRunBaseSubscription.getStartDate(), startDate);
+
+ assertEquals(realRunBaseSubscription.getState(), EntitlementState.CANCELLED);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testBPRepairReplaceCreateBeforeTrial() throws Exception {
+ final String baseProduct = "Shotgun";
+ final String newBaseProduct = "Assault-Rifle";
+
+ final DateTime startDate = clock.getUTCNow();
+ final int clockShift = -1;
+ final DateTime restartDate = startDate.plusDays(clockShift).minusDays(1);
+ final LinkedList<ExistingEvent> expected = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, newBaseProduct, PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, restartDate));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, newBaseProduct, PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, restartDate.plusDays(30)));
+
+ testBPRepairCreate(true, startDate, clockShift, baseProduct, newBaseProduct, expected);
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testBPRepairReplaceCreateInTrial() throws Exception {
+ final String baseProduct = "Shotgun";
+ final String newBaseProduct = "Assault-Rifle";
+
+ final DateTime startDate = clock.getUTCNow();
+ final int clockShift = 10;
+ final DateTime restartDate = startDate.plusDays(clockShift).minusDays(1);
+ final LinkedList<ExistingEvent> expected = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, newBaseProduct, PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, restartDate));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, newBaseProduct, PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, restartDate.plusDays(30)));
+
+ final UUID baseSubscriptionId = testBPRepairCreate(true, startDate, clockShift, baseProduct, newBaseProduct, expected);
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(32));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // CHECK WHAT"S GOING ON AFTER WE MOVE CLOCK-- FUTURE MOTIFICATION SHOULD KICK IN
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscriptionId, internalCallContext);
+
+ assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+ assertEquals(subscription.getBundleId(), bundle.getId());
+ assertEquals(subscription.getStartDate(), restartDate);
+ assertEquals(subscription.getBundleStartDate(), restartDate);
+
+ final Plan currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), newBaseProduct);
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ final PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testBPRepairReplaceCreateAfterTrial() throws Exception {
+ final String baseProduct = "Shotgun";
+ final String newBaseProduct = "Assault-Rifle";
+
+ final DateTime startDate = clock.getUTCNow();
+ final int clockShift = 40;
+ final DateTime restartDate = startDate.plusDays(clockShift).minusDays(1);
+ final LinkedList<ExistingEvent> expected = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, newBaseProduct, PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, restartDate));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, newBaseProduct, PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, restartDate.plusDays(30)));
+
+ testBPRepairCreate(false, startDate, clockShift, baseProduct, newBaseProduct, expected);
+ assertListenerStatus();
+ }
+
+ private UUID testBPRepairCreate(final boolean inTrial, final DateTime startDate, final int clockShift,
+ final String baseProduct, final String newBaseProduct, final List<ExistingEvent> expectedEvents) throws Exception {
+ // CREATE BP
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate);
+
+ // MOVE CLOCK
+ if (clockShift > 0) {
+ if (!inTrial) {
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ }
+
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(clockShift));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ if (!inTrial) {
+ assertListenerStatus();
+ }
+ }
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ final DateTime newCreateTime = baseSubscription.getStartDate().plusDays(clockShift - 1);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(newBaseProduct, ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL);
+
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CREATE, newCreateTime, spec);
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId()));
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId()));
+
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ // FIRST ISSUE DRY RUN
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ boolean dryRun = true;
+ final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+ List<SubscriptionBaseTimeline> subscriptionRepair = dryRunBundleRepair.getSubscriptions();
+ assertEquals(subscriptionRepair.size(), 1);
+ SubscriptionBaseTimeline cur = subscriptionRepair.get(0);
+ assertEquals(cur.getId(), baseSubscription.getId());
+
+ List<ExistingEvent> events = cur.getExistingEvents();
+ assertEquals(expectedEvents.size(), events.size());
+ int index = 0;
+ for (final ExistingEvent e : expectedEvents) {
+ testUtil.validateExistingEventForAssertion(e, events.get(index++));
+ }
+ final DefaultSubscriptionBase dryRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ assertEquals(dryRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+ assertEquals(dryRunBaseSubscription.getBundleId(), bundle.getId());
+ assertTrue(dryRunBaseSubscription.getStartDate().compareTo(baseSubscription.getStartDate()) == 0);
+
+ Plan currentPlan = dryRunBaseSubscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), baseProduct);
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ PlanPhase currentPhase = dryRunBaseSubscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ if (inTrial) {
+ assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL);
+ } else {
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+ }
+
+ // SECOND RE-ISSUE CALL-- NON DRY RUN
+ dryRun = false;
+ testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
+ final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+ assertListenerStatus();
+ subscriptionRepair = realRunBundleRepair.getSubscriptions();
+ assertEquals(subscriptionRepair.size(), 1);
+ cur = subscriptionRepair.get(0);
+ assertEquals(cur.getId(), baseSubscription.getId());
+
+ events = cur.getExistingEvents();
+ for (final ExistingEvent e : events) {
+ log.info(String.format("%s, %s, %s, %s", e.getSubscriptionTransitionType(), e.getEffectiveDate(), e.getPlanPhaseSpecifier().getProductName(), e.getPlanPhaseSpecifier().getPhaseType()));
+ }
+ assertEquals(events.size(), expectedEvents.size());
+ index = 0;
+ for (final ExistingEvent e : expectedEvents) {
+ testUtil.validateExistingEventForAssertion(e, events.get(index++));
+ }
+ final DefaultSubscriptionBase realRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(realRunBaseSubscription.getAllTransitions().size(), 2);
+
+ assertEquals(realRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+ assertEquals(realRunBaseSubscription.getBundleId(), bundle.getId());
+ assertEquals(realRunBaseSubscription.getStartDate(), newCreateTime);
+
+ currentPlan = realRunBaseSubscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), newBaseProduct);
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ currentPhase = realRunBaseSubscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL);
+
+ return baseSubscription.getId();
+ }
+
+ @Test(groups = "slow")
+ public void testBPRepairAddChangeInTrial() throws Exception {
+ final String baseProduct = "Shotgun";
+ final String newBaseProduct = "Assault-Rifle";
+
+ final DateTime startDate = clock.getUTCNow();
+ final int clockShift = 10;
+ final DateTime changeDate = startDate.plusDays(clockShift).minusDays(1);
+ final LinkedList<ExistingEvent> expected = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, baseProduct, PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, startDate));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CHANGE, newBaseProduct, PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, changeDate));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, newBaseProduct, PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, startDate.plusDays(30)));
+
+ final UUID baseSubscriptionId = testBPRepairAddChange(true, startDate, clockShift, baseProduct, newBaseProduct, expected, 3);
+
+ // CHECK WHAT"S GOING ON AFTER WE MOVE CLOCK-- FUTURE MOTIFICATION SHOULD KICK IN
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(32));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscriptionId, internalCallContext);
+
+ assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+ assertEquals(subscription.getBundleId(), bundle.getId());
+ assertEquals(subscription.getStartDate(), startDate);
+ assertEquals(subscription.getBundleStartDate(), startDate);
+
+ final Plan currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), newBaseProduct);
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ final PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testBPRepairAddChangeAfterTrial() throws Exception {
+ final String baseProduct = "Shotgun";
+ final String newBaseProduct = "Assault-Rifle";
+
+ final DateTime startDate = clock.getUTCNow();
+ final int clockShift = 40;
+ final DateTime changeDate = startDate.plusDays(clockShift).minusDays(1);
+
+ final LinkedList<ExistingEvent> expected = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, baseProduct, PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, startDate));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, baseProduct, PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, startDate.plusDays(30)));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CHANGE, newBaseProduct, PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, changeDate));
+ testBPRepairAddChange(false, startDate, clockShift, baseProduct, newBaseProduct, expected, 3);
+
+ assertListenerStatus();
+ }
+
+ private UUID testBPRepairAddChange(final boolean inTrial, final DateTime startDate, final int clockShift,
+ final String baseProduct, final String newBaseProduct, final List<ExistingEvent> expectedEvents, final int expectedTransitions) throws Exception {
+ // CREATE BP
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate);
+
+ // MOVE CLOCK
+ if (!inTrial) {
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ }
+
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(clockShift));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ if (!inTrial) {
+ assertListenerStatus();
+ }
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ final DateTime changeTime = baseSubscription.getStartDate().plusDays(clockShift - 1);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(newBaseProduct, ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL);
+
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, changeTime, spec);
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ if (inTrial) {
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId()));
+ }
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ // FIRST ISSUE DRY RUN
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ boolean dryRun = true;
+ final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+
+ List<SubscriptionBaseTimeline> subscriptionRepair = dryRunBundleRepair.getSubscriptions();
+ assertEquals(subscriptionRepair.size(), 1);
+ SubscriptionBaseTimeline cur = subscriptionRepair.get(0);
+ assertEquals(cur.getId(), baseSubscription.getId());
+
+ List<ExistingEvent> events = cur.getExistingEvents();
+ assertEquals(expectedEvents.size(), events.size());
+ int index = 0;
+ for (final ExistingEvent e : expectedEvents) {
+ testUtil.validateExistingEventForAssertion(e, events.get(index++));
+ }
+ final DefaultSubscriptionBase dryRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ assertEquals(dryRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+ assertEquals(dryRunBaseSubscription.getBundleId(), bundle.getId());
+ assertEquals(dryRunBaseSubscription.getStartDate(), baseSubscription.getStartDate());
+
+ Plan currentPlan = dryRunBaseSubscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), baseProduct);
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ PlanPhase currentPhase = dryRunBaseSubscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ if (inTrial) {
+ assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL);
+ } else {
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+ }
+
+ // SECOND RE-ISSUE CALL-- NON DRY RUN
+ dryRun = false;
+ testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
+ final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+ assertListenerStatus();
+
+ subscriptionRepair = realRunBundleRepair.getSubscriptions();
+ assertEquals(subscriptionRepair.size(), 1);
+ cur = subscriptionRepair.get(0);
+ assertEquals(cur.getId(), baseSubscription.getId());
+
+ events = cur.getExistingEvents();
+ assertEquals(expectedEvents.size(), events.size());
+ index = 0;
+ for (final ExistingEvent e : expectedEvents) {
+ testUtil.validateExistingEventForAssertion(e, events.get(index++));
+ }
+ final DefaultSubscriptionBase realRunBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(realRunBaseSubscription.getAllTransitions().size(), expectedTransitions);
+
+ assertEquals(realRunBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+ assertEquals(realRunBaseSubscription.getBundleId(), bundle.getId());
+ assertEquals(realRunBaseSubscription.getStartDate(), baseSubscription.getStartDate());
+
+ currentPlan = realRunBaseSubscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), newBaseProduct);
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ currentPhase = realRunBaseSubscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ if (inTrial) {
+ assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL);
+ } else {
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+ }
+ return baseSubscription.getId();
+ }
+
+ @Test(groups = "slow")
+ public void testRepairWithFutureCancelEvent() throws Exception {
+ final DateTime startDate = clock.getUTCNow();
+
+ // CREATE BP
+ SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate);
+
+ // MOVE CLOCK -- OUT OF TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(35));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // SET CTD to BASE SUBSCRIPTION SP CANCEL OCCURS EOT
+ final DateTime newChargedThroughDate = baseSubscription.getStartDate().plusDays(30).plusMonths(1);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate, internalCallContext);
+ baseSubscription = subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ baseSubscription.changePlan("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, callContext);
+
+ // CHECK CHANGE DID NOT OCCUR YET
+ Plan currentPlan = baseSubscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), "Shotgun");
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ final DateTime repairTime = clock.getUTCNow().minusDays(1);
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, repairTime, spec);
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(2).getEventId()));
+
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ // SKIP DRY RUN AND DO REPAIR...
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ final boolean dryRun = false;
+ testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
+ repairApi.repairBundle(bRepair, dryRun, callContext);
+ assertListenerStatus();
+
+ baseSubscription = subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ assertEquals(((DefaultSubscriptionBase) baseSubscription).getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+ assertEquals(baseSubscription.getBundleId(), bundle.getId());
+ assertEquals(baseSubscription.getStartDate(), baseSubscription.getStartDate());
+
+ currentPlan = baseSubscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), "Assault-Rifle");
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ final PlanPhase currentPhase = baseSubscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ assertListenerStatus();
+ }
+
+ // Needs real SQL backend to be tested properly
+ @Test(groups = "slow")
+ public void testENT_REPAIR_VIEW_CHANGED_newEvent() throws Exception {
+ final TestWithException test = new TestWithException();
+ final DateTime startDate = clock.getUTCNow();
+
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate);
+
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException {
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec);
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId()));
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId()));
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ final DateTime changeTime = clock.getUTCNow();
+ baseSubscription.changePlanWithDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, changeTime, callContext);
+ assertListenerStatus();
+
+ repairApi.repairBundle(bRepair, true, callContext);
+ assertListenerStatus();
+ }
+ }, ErrorCode.SUB_REPAIR_VIEW_CHANGED);
+ }
+
+ @Test(groups = "slow")
+ public void testENT_REPAIR_VIEW_CHANGED_ctd() throws Exception {
+ final TestWithException test = new TestWithException();
+ final DateTime startDate = clock.getUTCNow();
+
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate);
+
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException {
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec);
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId()));
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId()));
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ final DateTime newChargedThroughDate = baseSubscription.getStartDate().plusDays(30).plusMonths(1);
+
+ // Move clock at least a sec to make sure the last_sys_update from bundle is different-- and therefore generates a different viewId
+ clock.setDeltaFromReality(1000);
+
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate, internalCallContext);
+ subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ repairApi.repairBundle(bRepair, true, callContext);
+
+ assertListenerStatus();
+ }
+ }, ErrorCode.SUB_REPAIR_VIEW_CHANGED);
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithAO.java b/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithAO.java
new file mode 100644
index 0000000..19346a2
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithAO.java
@@ -0,0 +1,726 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.DeletedEvent;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.ExistingEvent;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.NewEvent;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionEvents;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+public class TestRepairWithAO extends SubscriptionTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testRepairChangeBPWithAddonIncluded() throws Exception {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ final DefaultSubscriptionBase aoSubscription2 = testUtil.createSubscription(bundle, "Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ // Quick check
+ SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 2);
+
+ SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ SubscriptionBaseTimeline aoRepair2 = testUtil.getSubscriptionRepair(aoSubscription2.getId(), bundleRepair);
+ assertEquals(aoRepair2.getExistingEvents().size(), 2);
+
+ final DateTime bpChangeDate = clock.getUTCNow().minusDays(1);
+
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(bpRepair.getExistingEvents().get(1).getEventId()));
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, bpChangeDate, spec);
+
+ bpRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ bundleRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(bpRepair));
+
+ boolean dryRun = true;
+ final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext);
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ aoRepair2 = testUtil.getSubscriptionRepair(aoSubscription2.getId(), dryRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), dryRunBundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 3);
+
+ // Check expected for AO
+ final List<ExistingEvent> expectedAO = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription.getStartDate()));
+ expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, "Telescopic-Scope", PhaseType.DISCOUNT,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, bpChangeDate));
+ int index = 0;
+ for (final ExistingEvent e : expectedAO) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+
+ final List<ExistingEvent> expectedAO2 = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expectedAO2.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Laser-Scope", PhaseType.DISCOUNT,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription2.getStartDate()));
+ expectedAO2.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Laser-Scope", PhaseType.EVERGREEN,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription2.getStartDate().plusMonths(1)));
+ index = 0;
+ for (final ExistingEvent e : expectedAO2) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair2.getExistingEvents().get(index++));
+ }
+
+ // Check expected for BP
+ final List<ExistingEvent> expectedBP = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Shotgun", PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, baseSubscription.getStartDate()));
+ expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CHANGE, "Assault-Rifle", PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, bpChangeDate));
+ expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Assault-Rifle", PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusDays(30)));
+ index = 0;
+ for (final ExistingEvent e : expectedBP) {
+ testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++));
+ }
+
+ DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 2);
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+
+ DefaultSubscriptionBase newAoSubscription2 = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription2.getId(), internalCallContext);
+ assertEquals(newAoSubscription2.getState(), EntitlementState.ACTIVE);
+ assertEquals(newAoSubscription2.getAllTransitions().size(), 2);
+ assertEquals(newAoSubscription2.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+
+ DefaultSubscriptionBase newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newBaseSubscription.getAllTransitions().size(), 2);
+ assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+
+ dryRun = false;
+ testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
+ final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext);
+ assertListenerStatus();
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), realRunBundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 3);
+
+ index = 0;
+ for (final ExistingEvent e : expectedAO) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+
+ index = 0;
+ for (final ExistingEvent e : expectedAO2) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair2.getExistingEvents().get(index++));
+ }
+
+ index = 0;
+ for (final ExistingEvent e : expectedBP) {
+ testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++));
+ }
+
+ newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.CANCELLED);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 2);
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+
+ newAoSubscription2 = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription2.getId(), internalCallContext);
+ assertEquals(newAoSubscription2.getState(), EntitlementState.ACTIVE);
+ assertEquals(newAoSubscription2.getAllTransitions().size(), 2);
+ assertEquals(newAoSubscription2.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+
+ newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newBaseSubscription.getAllTransitions().size(), 3);
+ assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+ }
+
+ @Test(groups = "slow")
+ public void testRepairChangeBPWithAddonNonAvailable() throws Exception {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ // MOVE CLOCK A LITTLE BIT MORE -- AFTER TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(32));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ // Quick check
+ SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 2);
+
+ SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ final DateTime bpChangeDate = clock.getUTCNow().minusDays(1);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, bpChangeDate, spec);
+
+ bpRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), Collections.<SubscriptionBaseTimeline.DeletedEvent>emptyList(), Collections.singletonList(ne));
+
+ bundleRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(bpRepair));
+
+ boolean dryRun = true;
+ final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext);
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 3);
+
+ bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), dryRunBundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 3);
+
+ // Check expected for AO
+ final List<ExistingEvent> expectedAO = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription.getStartDate()));
+ expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.EVERGREEN,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusMonths(1)));
+ expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, "Telescopic-Scope", PhaseType.EVERGREEN,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, bpChangeDate));
+ int index = 0;
+ for (final ExistingEvent e : expectedAO) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+
+ // Check expected for BP
+ final List<ExistingEvent> expectedBP = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Shotgun", PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, baseSubscription.getStartDate()));
+ expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Shotgun", PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusDays(30)));
+ expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CHANGE, "Pistol", PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, bpChangeDate));
+ index = 0;
+ for (final ExistingEvent e : expectedBP) {
+ testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++));
+ }
+
+ DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 2);
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+
+ DefaultSubscriptionBase newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newBaseSubscription.getAllTransitions().size(), 2);
+ assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+
+ dryRun = false;
+ testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
+ final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext);
+ assertListenerStatus();
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 3);
+
+ bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), realRunBundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 3);
+
+ index = 0;
+ for (final ExistingEvent e : expectedAO) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+
+ index = 0;
+ for (final ExistingEvent e : expectedBP) {
+ testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++));
+ }
+
+ newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.CANCELLED);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 3);
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+
+ newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newBaseSubscription.getAllTransitions().size(), 3);
+ assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+ }
+
+ @Test(groups = "slow")
+ public void testRepairCancelBP_EOT_WithAddons() throws Exception {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ // MOVE CLOCK A LITTLE BIT MORE -- AFTER TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(40));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // SET CTD to BASE SUBSCRIPTION SP CANCEL OCCURS EOT
+ final DateTime newChargedThroughDate = baseSubscription.getStartDate().plusDays(30).plusMonths(1);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate, internalCallContext);
+ baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ // Quick check
+ SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 2);
+
+ SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ final DateTime bpCancelDate = clock.getUTCNow().minusDays(1);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CANCEL, bpCancelDate, null);
+ bpRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), Collections.<SubscriptionBaseTimeline.DeletedEvent>emptyList(), Collections.singletonList(ne));
+ bundleRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(bpRepair));
+
+ boolean dryRun = true;
+ final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext);
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 3);
+
+ bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), dryRunBundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 3);
+
+ // Check expected for AO
+ final List<ExistingEvent> expectedAO = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription.getStartDate()));
+ expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Telescopic-Scope", PhaseType.EVERGREEN,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusMonths(1)));
+ expectedAO.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, "Telescopic-Scope", PhaseType.EVERGREEN,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, bpCancelDate));
+
+ int index = 0;
+ for (final ExistingEvent e : expectedAO) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+
+ // Check expected for BP
+ final List<ExistingEvent> expectedBP = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Shotgun", PhaseType.TRIAL,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.NO_BILLING_PERIOD, baseSubscription.getStartDate()));
+ expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Shotgun", PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusDays(30)));
+ expectedBP.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, "Shotgun", PhaseType.EVERGREEN,
+ ProductCategory.BASE, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, bpCancelDate));
+ index = 0;
+ for (final ExistingEvent e : expectedBP) {
+ testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++));
+ }
+
+ DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 2);
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+
+ DefaultSubscriptionBase newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newBaseSubscription.getAllTransitions().size(), 2);
+ assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+
+ dryRun = false;
+ testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
+ final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bundleRepair, dryRun, callContext);
+ assertListenerStatus();
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 3);
+
+ bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), realRunBundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 3);
+
+ index = 0;
+ for (final ExistingEvent e : expectedAO) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+
+ index = 0;
+ for (final ExistingEvent e : expectedBP) {
+ testUtil.validateExistingEventForAssertion(e, bpRepair.getExistingEvents().get(index++));
+ }
+
+ newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.CANCELLED);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 3);
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+
+ newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(newBaseSubscription.getState(), EntitlementState.CANCELLED);
+ assertEquals(newBaseSubscription.getAllTransitions().size(), 3);
+ assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+
+ }
+
+ @Test(groups = "slow")
+ public void testRepairCancelAO() throws Exception {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ // Quick check
+ SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 2);
+
+ SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(1).getEventId()));
+ final DateTime aoCancelDate = aoSubscription.getStartDate().plusDays(1);
+
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CANCEL, aoCancelDate, null);
+
+ final SubscriptionBaseTimeline saoRepair = testUtil.createSubscriptionRepair(aoSubscription.getId(), des, Collections.singletonList(ne));
+
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(saoRepair));
+
+ boolean dryRun = true;
+ final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 2);
+
+ final List<ExistingEvent> expected = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription.getStartDate()));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CANCEL, "Telescopic-Scope", PhaseType.DISCOUNT,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoCancelDate));
+ int index = 0;
+ for (final ExistingEvent e : expected) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+ DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 2);
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+
+ DefaultSubscriptionBase newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newBaseSubscription.getAllTransitions().size(), 2);
+ assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+
+ dryRun = false;
+ testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
+ final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+ assertListenerStatus();
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+ index = 0;
+ for (final ExistingEvent e : expected) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+
+ newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.CANCELLED);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 2);
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+
+ newBaseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(newBaseSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newBaseSubscription.getAllTransitions().size(), 2);
+ assertEquals(newBaseSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+ }
+
+ @Test(groups = "slow")
+ public void testRepairRecreateAO() throws Exception {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ // Quick check
+ final SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 2);
+
+ SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(0).getEventId()));
+ des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(1).getEventId()));
+
+ final DateTime aoRecreateDate = aoSubscription.getStartDate().plusDays(1);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.DISCOUNT);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CREATE, aoRecreateDate, spec);
+
+ final SubscriptionBaseTimeline saoRepair = testUtil.createSubscriptionRepair(aoSubscription.getId(), des, Collections.singletonList(ne));
+
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(saoRepair));
+
+ boolean dryRun = true;
+ final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ final List<ExistingEvent> expected = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoRecreateDate));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Telescopic-Scope", PhaseType.EVERGREEN,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, baseSubscription.getStartDate().plusMonths(1) /* Bundle align */));
+ int index = 0;
+ for (final ExistingEvent e : expected) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+ DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 2);
+ assertEquals(newAoSubscription.getStartDate(), aoSubscription.getStartDate());
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+
+ // NOW COMMIT
+ dryRun = false;
+ testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
+ final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+ assertListenerStatus();
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+ index = 0;
+ for (final ExistingEvent e : expected) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+
+ newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 2);
+ assertEquals(newAoSubscription.getStartDate(), aoRecreateDate);
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+
+ }
+
+ // Fasten your seatbelt here:
+ //
+ // We are doing repair for multi-phase tiered-addon with different alignment:
+ // Telescopic-Scope -> Laser-Scope
+ // Tiered ADON logic
+ // . Both multi phase
+ // . Telescopic-Scope (bundle align) and Laser-Scope is SubscriptionBase align
+ //
+ @Test(groups = "slow")
+ public void testRepairChangeAOOK() throws Exception {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ // Quick check
+ final SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 2);
+
+ SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(1).getEventId()));
+ final DateTime aoChangeDate = aoSubscription.getStartDate().plusDays(1);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Laser-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, aoChangeDate, spec);
+
+ final SubscriptionBaseTimeline saoRepair = testUtil.createSubscriptionRepair(aoSubscription.getId(), des, Collections.singletonList(ne));
+
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(saoRepair));
+
+ boolean dryRun = true;
+ final BundleBaseTimeline dryRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), dryRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 3);
+
+ final List<ExistingEvent> expected = new LinkedList<SubscriptionBaseTimeline.ExistingEvent>();
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CREATE, "Telescopic-Scope", PhaseType.DISCOUNT,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoSubscription.getStartDate()));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.CHANGE, "Laser-Scope", PhaseType.DISCOUNT,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY, aoChangeDate));
+ expected.add(testUtil.createExistingEventForAssertion(SubscriptionBaseTransitionType.PHASE, "Laser-Scope", PhaseType.EVERGREEN,
+ ProductCategory.ADD_ON, PriceListSet.DEFAULT_PRICELIST_NAME, BillingPeriod.MONTHLY,
+ aoSubscription.getStartDate().plusMonths(1) /* SubscriptionBase alignment */));
+
+ int index = 0;
+ for (final ExistingEvent e : expected) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+ DefaultSubscriptionBase newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 2);
+
+ // AND NOW COMMIT
+ dryRun = false;
+ testListener.pushExpectedEvent(NextEvent.REPAIR_BUNDLE);
+ final BundleBaseTimeline realRunBundleRepair = repairApi.repairBundle(bRepair, dryRun, callContext);
+ assertListenerStatus();
+
+ aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), realRunBundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 3);
+ index = 0;
+ for (final ExistingEvent e : expected) {
+ testUtil.validateExistingEventForAssertion(e, aoRepair.getExistingEvents().get(index++));
+ }
+
+ newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(newAoSubscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(newAoSubscription.getAllTransitions().size(), 3);
+
+ assertEquals(newAoSubscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION + 1);
+ assertEquals(newAoSubscription.getBundleId(), bundle.getId());
+ assertEquals(newAoSubscription.getStartDate(), aoSubscription.getStartDate());
+
+ final Plan currentPlan = newAoSubscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), "Laser-Scope");
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.ADD_ON);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ PlanPhase currentPhase = newAoSubscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ // One phase for BP an one phase for the new AO (laser-scope)
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(60));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ newAoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ currentPhase = newAoSubscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithError.java b/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithError.java
new file mode 100644
index 0000000..dbd4f57
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/timeline/TestRepairWithError.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.timeline;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.subscription.SubscriptionTestSuiteNoDB;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.DeletedEvent;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.NewEvent;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.user.TestSubscriptionHelper.TestWithException;
+import org.killbill.billing.subscription.api.user.TestSubscriptionHelper.TestWithExceptionCallback;
+
+import static org.testng.Assert.assertEquals;
+
+public class TestRepairWithError extends SubscriptionTestSuiteNoDB {
+
+ private static final String baseProduct = "Shotgun";
+ private TestWithException test;
+ private SubscriptionBase baseSubscription;
+
+ @Override
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ test = new TestWithException();
+ final DateTime startDate = clock.getUTCNow();
+ baseSubscription = testUtil.createSubscription(bundle, baseProduct, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, startDate);
+ }
+
+ @Test(groups = "fast")
+ public void testENT_REPAIR_NEW_EVENT_BEFORE_LAST_BP_REMAINING() throws Exception {
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException {
+ // MOVE AFTER TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(40));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ assertListenerStatus();
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec);
+
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), Collections.<DeletedEvent>emptyList(), Collections.singletonList(ne));
+
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ repairApi.repairBundle(bRepair, true, callContext);
+ }
+ }, ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_BP_REMAINING);
+ }
+
+ @Test(groups = "fast")
+ public void testENT_REPAIR_INVALID_DELETE_SET() throws Exception {
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException {
+
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ final DateTime changeTime = clock.getUTCNow();
+ baseSubscription.changePlanWithDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, changeTime, callContext);
+ assertListenerStatus();
+
+ // MOVE AFTER TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(40));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec);
+ final DeletedEvent de = testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId());
+
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), Collections.singletonList(de), Collections.singletonList(ne));
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ repairApi.repairBundle(bRepair, true, callContext);
+ }
+ }, ErrorCode.SUB_REPAIR_INVALID_DELETE_SET);
+ }
+
+ @Test(groups = "fast")
+ public void testENT_REPAIR_NON_EXISTENT_DELETE_EVENT() throws Exception {
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException {
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec);
+ final DeletedEvent de = testUtil.createDeletedEvent(UUID.randomUUID());
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), Collections.singletonList(de), Collections.singletonList(ne));
+
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ repairApi.repairBundle(bRepair, true, callContext);
+ }
+ }, ErrorCode.SUB_REPAIR_NON_EXISTENT_DELETE_EVENT);
+ }
+
+ @Test(groups = "fast")
+ public void testENT_REPAIR_SUB_RECREATE_NOT_EMPTY() throws Exception {
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException {
+
+ // MOVE AFTER TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(40));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CREATE, baseSubscription.getStartDate().plusDays(10), spec);
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId()));
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ repairApi.repairBundle(bRepair, true, callContext);
+
+ }
+ }, ErrorCode.SUB_REPAIR_SUB_RECREATE_NOT_EMPTY);
+ }
+
+ @Test(groups = "fast")
+ public void testENT_REPAIR_SUB_EMPTY() throws Exception {
+ test.withException(new TestWithExceptionCallback() {
+
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException {
+
+ // MOVE AFTER TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(40));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CHANGE, baseSubscription.getStartDate().plusDays(10), spec);
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId()));
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId()));
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ repairApi.repairBundle(bRepair, true, callContext);
+ }
+ }, ErrorCode.SUB_REPAIR_SUB_EMPTY);
+ }
+
+ @Test(groups = "fast")
+ public void testENT_REPAIR_AO_CREATE_BEFORE_BP_START() throws Exception {
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException {
+ // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ // Quick check
+ final SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 2);
+
+ final SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(0).getEventId()));
+ des.add(testUtil.createDeletedEvent(aoRepair.getExistingEvents().get(1).getEventId()));
+
+ final DateTime aoRecreateDate = aoSubscription.getStartDate().minusDays(5);
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.DISCOUNT);
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CREATE, aoRecreateDate, spec);
+
+ final SubscriptionBaseTimeline saoRepair = testUtil.createSubscriptionRepair(aoSubscription.getId(), des, Collections.singletonList(ne));
+
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(saoRepair));
+
+ final boolean dryRun = true;
+ repairApi.repairBundle(bRepair, dryRun, callContext);
+ }
+ }, ErrorCode.SUB_REPAIR_AO_CREATE_BEFORE_BP_START);
+ }
+
+ @Test(groups = "fast")
+ public void testENT_REPAIR_NEW_EVENT_BEFORE_LAST_AO_REMAINING() throws Exception {
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException {
+
+ // MOVE CLOCK A LITTLE BIT-- STILL IN TRIAL
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ // MOVE CLOCK A LITTLE BIT MORE -- STILL IN TRIAL
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ // Quick check
+ final SubscriptionBaseTimeline bpRepair = testUtil.getSubscriptionRepair(baseSubscription.getId(), bundleRepair);
+ assertEquals(bpRepair.getExistingEvents().size(), 2);
+
+ final SubscriptionBaseTimeline aoRepair = testUtil.getSubscriptionRepair(aoSubscription.getId(), bundleRepair);
+ assertEquals(aoRepair.getExistingEvents().size(), 2);
+
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ //des.add(createDeletedEvent(aoRepair.getExistingEvents().get(1).getEventId()));
+ final DateTime aoCancelDate = aoSubscription.getStartDate().plusDays(10);
+
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CANCEL, aoCancelDate, null);
+
+ final SubscriptionBaseTimeline saoRepair = testUtil.createSubscriptionRepair(aoSubscription.getId(), des, Collections.singletonList(ne));
+
+ bundleRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(saoRepair));
+
+ final boolean dryRun = true;
+ repairApi.repairBundle(bundleRepair, dryRun, callContext);
+ }
+ }, ErrorCode.SUB_REPAIR_NEW_EVENT_BEFORE_LAST_AO_REMAINING);
+ }
+
+ @Test(groups = "fast", enabled = false) // TODO - fails on jdk7 on Travis
+ public void testENT_REPAIR_BP_RECREATE_MISSING_AO() throws Exception {
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException {
+
+ //testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ //assertListenerStatus();
+
+ final DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, "Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ final BundleBaseTimeline bundleRepair = repairApi.getBundleTimeline(bundle.getId(), callContext);
+ testUtil.sortEventsOnBundle(bundleRepair);
+
+ final DateTime newCreateTime = baseSubscription.getStartDate().plusDays(3);
+
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL);
+
+ final NewEvent ne = testUtil.createNewEvent(SubscriptionBaseTransitionType.CREATE, newCreateTime, spec);
+ final List<DeletedEvent> des = new LinkedList<SubscriptionBaseTimeline.DeletedEvent>();
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId()));
+ des.add(testUtil.createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId()));
+
+ final SubscriptionBaseTimeline sRepair = testUtil.createSubscriptionRepair(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ // FIRST ISSUE DRY RUN
+ final BundleBaseTimeline bRepair = testUtil.createBundleRepair(bundle.getId(), bundleRepair.getViewId(), Collections.singletonList(sRepair));
+
+ final boolean dryRun = true;
+ repairApi.repairBundle(bRepair, dryRun, callContext);
+ }
+ }, ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO);
+ }
+
+ //
+ // CAN'T seem to trigger such case easily, other errors trigger before...
+ //
+ @Test(groups = "fast", enabled = false)
+ public void testENT_REPAIR_BP_RECREATE_MISSING_AO_CREATE() throws Exception {
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException {
+ /*
+ //testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(4));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+
+ DefaultSubscriptionBase aoSubscription = createSubscription("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ BundleRepair bundleRepair = repairApi.getBundleRepair(bundle.getId());
+ sortEventsOnBundle(bundleRepair);
+
+ DateTime newCreateTime = baseSubscription.getStartDate().plusDays(3);
+
+ PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL);
+
+ NewEvent ne = createNewEvent(SubscriptionBaseTransitionType.CREATE, newCreateTime, spec);
+ List<DeletedEvent> des = new LinkedList<SubscriptionRepair.DeletedEvent>();
+ des.add(createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(0).getEventId()));
+ des.add(createDeletedEvent(bundleRepair.getSubscriptions().get(0).getExistingEvents().get(1).getEventId()));
+
+ SubscriptionRepair bpRepair = createSubscriptionReapir(baseSubscription.getId(), des, Collections.singletonList(ne));
+
+ ne = createNewEvent(SubscriptionBaseTransitionType.CANCEL, clock.getUTCNow().minusDays(1), null);
+ SubscriptionRepair aoRepair = createSubscriptionReapir(aoSubscription.getId(), Collections.<SubscriptionRepair.DeletedEvent>emptyList(), Collections.singletonList(ne));
+
+
+ List<SubscriptionRepair> allRepairs = new LinkedList<SubscriptionRepair>();
+ allRepairs.add(bpRepair);
+ allRepairs.add(aoRepair);
+ bundleRepair = createBundleRepair(bundle.getId(), bundleRepair.getViewId(), allRepairs);
+ // FIRST ISSUE DRY RUN
+ BundleRepair bRepair = createBundleRepair(bundle.getId(), bundleRepair.getViewId(), allRepairs);
+
+ boolean dryRun = true;
+ repairApi.repairBundle(bRepair, dryRun, callcontext);
+ */
+ }
+ }, ErrorCode.SUB_REPAIR_BP_RECREATE_MISSING_AO_CREATE);
+ }
+
+ @Test(groups = "fast", enabled = false)
+ public void testENT_REPAIR_MISSING_AO_DELETE_EVENT() throws Exception {
+ test.withException(new TestWithExceptionCallback() {
+ @Override
+ public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException {
+
+ /*
+ // MOVE CLOCK -- JUST BEFORE END OF TRIAL
+ *
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(29));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ clock.setDeltaFromReality(getDurationDay(29), 0);
+
+ DefaultSubscriptionBase aoSubscription = createSubscription("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ // MOVE CLOCK -- RIGHT OUT OF TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.addDeltaFromReality(getDurationDay(5));
+ assertListenerStatus();
+
+ DateTime requestedChange = clock.getUTCNow();
+ baseSubscription.changePlanWithRequestedDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, requestedChange, callcontext);
+
+ DateTime reapairTime = clock.getUTCNow().minusDays(1);
+
+ BundleRepair bundleRepair = repairApi.getBundleRepair(bundle.getId());
+ sortEventsOnBundle(bundleRepair);
+
+ SubscriptionRepair bpRepair = getSubscriptionRepair(baseSubscription.getId(), bundleRepair);
+ SubscriptionRepair aoRepair = getSubscriptionRepair(aoSubscription.getId(), bundleRepair);
+
+ List<DeletedEvent> bpdes = new LinkedList<SubscriptionRepair.DeletedEvent>();
+ bpdes.add(createDeletedEvent(bpRepair.getExistingEvents().get(2).getEventId()));
+ bpRepair = createSubscriptionReapir(baseSubscription.getId(), bpdes, Collections.<NewEvent>emptyList());
+
+ NewEvent ne = createNewEvent(SubscriptionBaseTransitionType.CANCEL, reapairTime, null);
+ aoRepair = createSubscriptionReapir(aoSubscription.getId(), Collections.<SubscriptionRepair.DeletedEvent>emptyList(), Collections.singletonList(ne));
+
+ List<SubscriptionRepair> allRepairs = new LinkedList<SubscriptionRepair>();
+ allRepairs.add(bpRepair);
+ allRepairs.add(aoRepair);
+ bundleRepair = createBundleRepair(bundle.getId(), bundleRepair.getViewId(), allRepairs);
+
+ boolean dryRun = false;
+ repairApi.repairBundle(bundleRepair, dryRun, callcontext);
+ */
+ }
+ }, ErrorCode.SUB_REPAIR_MISSING_AO_DELETE_EVENT);
+ }
+
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
new file mode 100644
index 0000000..f4c88b8
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestDefaultSubscriptionTransferApi.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.transfer;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.catalog.MockCatalog;
+import org.killbill.billing.catalog.MockCatalogService;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.subscription.SubscriptionTestSuiteNoDB;
+import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.ExistingEvent;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineApi;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.user.ApiEventTransfer;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import com.google.common.collect.ImmutableList;
+
+// Simple unit tests for DefaultSubscriptionBaseTransferApi, see TestTransfer for more advanced tests with dao
+public class TestDefaultSubscriptionTransferApi extends SubscriptionTestSuiteNoDB {
+
+ private DefaultSubscriptionBaseTransferApi transferApi;
+
+ @Override
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ final NonEntityDao nonEntityDao = Mockito.mock(NonEntityDao.class);
+ final SubscriptionDao dao = Mockito.mock(SubscriptionDao.class);
+ final CatalogService catalogService = new MockCatalogService(new MockCatalog());
+ final SubscriptionBaseApiService apiService = Mockito.mock(SubscriptionBaseApiService.class);
+ final SubscriptionBaseTimelineApi timelineApi = Mockito.mock(SubscriptionBaseTimelineApi.class);
+ final InternalCallContextFactory internalCallContextFactory = new InternalCallContextFactory(clock, nonEntityDao, new CacheControllerDispatcher());
+ transferApi = new DefaultSubscriptionBaseTransferApi(clock, dao, timelineApi, catalogService, apiService, internalCallContextFactory);
+ }
+
+ @Test(groups = "fast")
+ public void testEventsForCancelledSubscriptionBeforeTransfer() throws Exception {
+ final DateTime subscriptionStartTime = clock.getUTCNow();
+ final DateTime subscriptionCancelTime = subscriptionStartTime.plusDays(1);
+ final ImmutableList<ExistingEvent> existingEvents = ImmutableList.<ExistingEvent>of(createEvent(subscriptionStartTime, SubscriptionBaseTransitionType.CREATE),
+ createEvent(subscriptionCancelTime, SubscriptionBaseTransitionType.CANCEL));
+ final SubscriptionBuilder subscriptionBuilder = new SubscriptionBuilder();
+ final DefaultSubscriptionBase subscription = new DefaultSubscriptionBase(subscriptionBuilder);
+
+ final DateTime transferDate = subscriptionStartTime.plusDays(10);
+ final List<SubscriptionBaseEvent> events = transferApi.toEvents(existingEvents, subscription, transferDate, callContext);
+
+ Assert.assertEquals(events.size(), 0);
+ }
+
+ @Test(groups = "fast")
+ public void testEventsForCancelledSubscriptionAfterTransfer() throws Exception {
+ final DateTime subscriptionStartTime = clock.getUTCNow();
+ final DateTime subscriptionCancelTime = subscriptionStartTime.plusDays(1);
+ final ImmutableList<ExistingEvent> existingEvents = ImmutableList.<ExistingEvent>of(createEvent(subscriptionStartTime, SubscriptionBaseTransitionType.CREATE),
+ createEvent(subscriptionCancelTime, SubscriptionBaseTransitionType.CANCEL));
+ final SubscriptionBuilder subscriptionBuilder = new SubscriptionBuilder();
+ final DefaultSubscriptionBase subscription = new DefaultSubscriptionBase(subscriptionBuilder);
+
+ final DateTime transferDate = subscriptionStartTime.plusHours(1);
+ final List<SubscriptionBaseEvent> events = transferApi.toEvents(existingEvents, subscription, transferDate, callContext);
+
+ Assert.assertEquals(events.size(), 1);
+ Assert.assertEquals(events.get(0).getType(), EventType.API_USER);
+ Assert.assertEquals(events.get(0).getEffectiveDate(), transferDate);
+ Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER);
+ }
+
+ @Test(groups = "fast")
+ public void testEventsAfterTransferForMigratedBundle1() throws Exception {
+ // MIGRATE_ENTITLEMENT then MIGRATE_BILLING (both in the past)
+ final DateTime transferDate = clock.getUTCNow();
+ final DateTime migrateSubscriptionEventEffectiveDate = transferDate.minusDays(10);
+ final DateTime migrateBillingEventEffectiveDate = migrateSubscriptionEventEffectiveDate.plusDays(1);
+ final List<SubscriptionBaseEvent> events = transferBundle(migrateSubscriptionEventEffectiveDate, migrateBillingEventEffectiveDate, transferDate);
+
+ Assert.assertEquals(events.size(), 1);
+ Assert.assertEquals(events.get(0).getType(), EventType.API_USER);
+ Assert.assertEquals(events.get(0).getEffectiveDate(), transferDate);
+ Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER);
+ }
+
+ @Test(groups = "fast")
+ public void testEventsAfterTransferForMigratedBundle2() throws Exception {
+ // MIGRATE_ENTITLEMENT and MIGRATE_BILLING at the same time (both in the past)
+ final DateTime transferDate = clock.getUTCNow();
+ final DateTime migrateSubscriptionEventEffectiveDate = transferDate.minusDays(10);
+ final DateTime migrateBillingEventEffectiveDate = migrateSubscriptionEventEffectiveDate;
+ final List<SubscriptionBaseEvent> events = transferBundle(migrateSubscriptionEventEffectiveDate, migrateBillingEventEffectiveDate, transferDate);
+
+ Assert.assertEquals(events.size(), 1);
+ Assert.assertEquals(events.get(0).getType(), EventType.API_USER);
+ Assert.assertEquals(events.get(0).getEffectiveDate(), transferDate);
+ Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER);
+ }
+
+ @Test(groups = "fast")
+ public void testEventsAfterTransferForMigratedBundle3() throws Exception {
+ // MIGRATE_ENTITLEMENT then MIGRATE_BILLING (the latter in the future)
+ final DateTime transferDate = clock.getUTCNow();
+ final DateTime migrateSubscriptionEventEffectiveDate = transferDate.minusDays(10);
+ final DateTime migrateBillingEventEffectiveDate = migrateSubscriptionEventEffectiveDate.plusDays(20);
+ final List<SubscriptionBaseEvent> events = transferBundle(migrateSubscriptionEventEffectiveDate, migrateBillingEventEffectiveDate, transferDate);
+
+ Assert.assertEquals(events.size(), 1);
+ Assert.assertEquals(events.get(0).getType(), EventType.API_USER);
+ Assert.assertEquals(events.get(0).getEffectiveDate(), transferDate);
+ Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER);
+ }
+
+ @Test(groups = "fast")
+ public void testEventsAfterTransferForMigratedBundle4() throws Exception {
+ // MIGRATE_ENTITLEMENT then MIGRATE_BILLING (both in the future)
+ final DateTime transferDate = clock.getUTCNow();
+ final DateTime migrateSubscriptionEventEffectiveDate = transferDate.plusDays(10);
+ final DateTime migrateBillingEventEffectiveDate = migrateSubscriptionEventEffectiveDate.plusDays(20);
+ final List<SubscriptionBaseEvent> events = transferBundle(migrateSubscriptionEventEffectiveDate, migrateBillingEventEffectiveDate, transferDate);
+
+ Assert.assertEquals(events.size(), 1);
+ Assert.assertEquals(events.get(0).getType(), EventType.API_USER);
+ Assert.assertEquals(events.get(0).getEffectiveDate(), migrateSubscriptionEventEffectiveDate);
+ Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER);
+ }
+
+ private List<SubscriptionBaseEvent> transferBundle(final DateTime migrateSubscriptionEventEffectiveDate, final DateTime migrateBillingEventEffectiveDate,
+ final DateTime transferDate) throws SubscriptionBaseTransferApiException {
+ final ImmutableList<ExistingEvent> existingEvents = createMigrateEvents(migrateSubscriptionEventEffectiveDate, migrateBillingEventEffectiveDate);
+ final SubscriptionBuilder subscriptionBuilder = new SubscriptionBuilder();
+ final DefaultSubscriptionBase subscription = new DefaultSubscriptionBase(subscriptionBuilder);
+
+ return transferApi.toEvents(existingEvents, subscription, transferDate, callContext);
+ }
+
+ private ExistingEvent createEvent(final DateTime eventEffectiveDate, final SubscriptionBaseTransitionType subscriptionTransitionType) {
+ return new ExistingEvent() {
+ @Override
+ public DateTime getEffectiveDate() {
+ return eventEffectiveDate;
+ }
+
+ @Override
+ public String getPlanPhaseName() {
+ return SubscriptionBaseTransitionType.CANCEL.equals(subscriptionTransitionType) ? null : "BicycleTrialEvergreen1USD-trial";
+ }
+
+ @Override
+ public UUID getEventId() {
+ return UUID.randomUUID();
+ }
+
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return SubscriptionBaseTransitionType.CANCEL.equals(subscriptionTransitionType) ? null :
+ new PlanPhaseSpecifier("BicycleTrialEvergreen1USD", ProductCategory.BASE, BillingPeriod.NO_BILLING_PERIOD,
+ PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.FIXEDTERM);
+ }
+
+ @Override
+ public DateTime getRequestedDate() {
+ return getEffectiveDate();
+ }
+
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return subscriptionTransitionType;
+ }
+ };
+ }
+
+ private ImmutableList<ExistingEvent> createMigrateEvents(final DateTime migrateSubscriptionEventEffectiveDate, final DateTime migrateBillingEventEffectiveDate) {
+ final ExistingEvent migrateEntitlementEvent = new ExistingEvent() {
+ @Override
+ public DateTime getEffectiveDate() {
+ return migrateSubscriptionEventEffectiveDate;
+ }
+
+ @Override
+ public String getPlanPhaseName() {
+ return "BicycleTrialEvergreen1USD-trial";
+ }
+
+ @Override
+ public UUID getEventId() {
+ return UUID.randomUUID();
+ }
+
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return new PlanPhaseSpecifier("BicycleTrialEvergreen1USD", ProductCategory.BASE, BillingPeriod.NO_BILLING_PERIOD,
+ PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.FIXEDTERM);
+ }
+
+ @Override
+ public DateTime getRequestedDate() {
+ return getEffectiveDate();
+ }
+
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return SubscriptionBaseTransitionType.MIGRATE_ENTITLEMENT;
+ }
+ };
+
+ final ExistingEvent migrateBillingEvent = new ExistingEvent() {
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return migrateBillingEventEffectiveDate;
+ }
+
+ @Override
+ public String getPlanPhaseName() {
+ return migrateEntitlementEvent.getPlanPhaseName();
+ }
+
+ @Override
+ public UUID getEventId() {
+ return UUID.randomUUID();
+ }
+
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return migrateEntitlementEvent.getPlanPhaseSpecifier();
+ }
+
+ @Override
+ public DateTime getRequestedDate() {
+ return migrateEntitlementEvent.getRequestedDate();
+ }
+
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return SubscriptionBaseTransitionType.MIGRATE_BILLING;
+ }
+ };
+
+ return ImmutableList.<ExistingEvent>of(migrateEntitlementEvent, migrateBillingEvent);
+ }
+}
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
new file mode 100644
index 0000000..af2277c
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.transfer;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.AccountMigration;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
+
+ protected static final Logger log = LoggerFactory.getLogger(TestTransfer.class);
+
+ @Test(groups = "slow")
+ public void testTransferMigratedSubscriptionWithCTDInFuture() throws Exception {
+ final UUID newAccountId = UUID.randomUUID();
+
+ final DateTime startDate = clock.getUTCNow().minusMonths(2);
+ final DateTime beforeMigration = clock.getUTCNow();
+ final AccountMigration toBeMigrated = testUtil.createAccountForMigrationWithRegularBasePlan(startDate);
+ final DateTime afterMigration = clock.getUTCNow();
+
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+ migrationApi.migrate(toBeMigrated, callContext);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseBundle> bundles = subscriptionInternalApi.getBundlesForAccount(toBeMigrated.getAccountKey(), internalCallContext);
+ assertEquals(bundles.size(), 1);
+ final SubscriptionBaseBundle bundle = bundles.get(0);
+
+ final DateTime bundleCreatedDate = bundle.getCreatedDate();
+
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+ final SubscriptionBase subscription = subscriptions.get(0);
+ testUtil.assertDateWithin(subscription.getStartDate(), beforeMigration.minusMonths(2), afterMigration.minusMonths(2));
+ assertEquals(subscription.getEndDate(), null);
+ assertEquals(subscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(subscription.getState(), EntitlementState.ACTIVE);
+ assertEquals(subscription.getCurrentPlan().getName(), "shotgun-annual");
+ assertEquals(subscription.getChargedThroughDate(), startDate.plusYears(1));
+ // WE should see MIGRATE_ENTITLEMENT and then MIGRATE_BILLING in the future
+ assertEquals(subscriptionInternalApi.getBillingTransitions(subscription, internalCallContext).size(), 1);
+ assertEquals(subscriptionInternalApi.getBillingTransitions(subscription, internalCallContext).get(0).getTransitionType(), SubscriptionBaseTransitionType.MIGRATE_BILLING);
+ assertTrue(subscriptionInternalApi.getBillingTransitions(subscription, internalCallContext).get(0).getEffectiveTransitionTime().compareTo(clock.getUTCNow()) > 0);
+ assertListenerStatus();
+
+ // MOVE A LITTLE, STILL IN TRIAL
+ clock.addDays(20);
+
+ final DateTime transferRequestedDate = clock.getUTCNow();
+
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ transferApi.transferBundle(bundle.getAccountId(), newAccountId, bundle.getExternalKey(), transferRequestedDate, false, true, callContext);
+ assertListenerStatus();
+
+ final SubscriptionBase oldBaseSubscription = subscriptionInternalApi.getBaseSubscription(bundle.getId(), internalCallContext);
+ assertTrue(oldBaseSubscription.getState() == EntitlementState.CANCELLED);
+ // The MIGRATE_BILLING event should have been invalidated
+ assertEquals(subscriptionInternalApi.getBillingTransitions(oldBaseSubscription, internalCallContext).size(), 0);
+ //assertEquals(subscriptionInternalApi.getBillingTransitions(oldBaseSubscription, internalCallContext).get(0).getTransitionType(), SubscriptionBaseTransitionType.CANCEL);
+ }
+
+ @Test(groups = "slow")
+ public void testTransferBPInTrialWithNoCTD() throws Exception {
+ final UUID newAccountId = UUID.randomUUID();
+
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ final DateTime evergreenPhaseDate = ((DefaultSubscriptionBase) baseSubscription).getPendingTransition().getEffectiveTransitionTime();
+
+ // MOVE A LITTLE, STILL IN TRIAL
+ clock.addDays(20);
+
+ final DateTime beforeTransferDate = clock.getUTCNow();
+ final DateTime transferRequestedDate = clock.getUTCNow();
+
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ transferApi.transferBundle(bundle.getAccountId(), newAccountId, bundle.getExternalKey(), transferRequestedDate, false, false, callContext);
+ assertListenerStatus();
+ final DateTime afterTransferDate = clock.getUTCNow();
+
+ // CHECK OLD BASE IS CANCEL AT THE TRANSFER DATE
+ final SubscriptionBase oldBaseSubscription = subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertNotNull(oldBaseSubscription.getEndDate());
+ testUtil.assertDateWithin(oldBaseSubscription.getEndDate(), beforeTransferDate, afterTransferDate);
+ assertTrue(oldBaseSubscription.getEndDate().compareTo(transferRequestedDate) == 0);
+
+ // CHECK NEW BUNDLE EXIST, WITH ONE SUBSCRIPTION STARTING ON TRANSFER_DATE
+ final List<SubscriptionBaseBundle> bundlesForAccountAndKey = subscriptionInternalApi.getBundlesForAccountAndKey(newAccountId, bundle.getExternalKey(), internalCallContext);
+ assertEquals(bundlesForAccountAndKey.size(), 1);
+
+ final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+
+ final SubscriptionBase newBaseSubscription = subscriptions.get(0);
+ assertTrue(((DefaultSubscriptionBase) newBaseSubscription).getAlignStartDate().compareTo(((DefaultSubscriptionBase) oldBaseSubscription).getAlignStartDate()) == 0);
+
+ // CHECK NEXT PENDING PHASE IS ALIGNED WITH OLD SUBSCRIPTION START DATE
+ assertEquals(subscriptionInternalApi.getAllTransitions(newBaseSubscription, internalCallContext).size(), 2);
+ assertTrue(subscriptionInternalApi.getAllTransitions(newBaseSubscription, internalCallContext).get(1).getEffectiveTransitionTime().compareTo(evergreenPhaseDate) == 0);
+
+ final Plan newPlan = newBaseSubscription.getCurrentPlan();
+ assertEquals(newPlan.getProduct().getName(), baseProduct);
+ assertEquals(newBaseSubscription.getCurrentPhase().getPhaseType(), PhaseType.TRIAL);
+ }
+
+ @Test(groups = "slow")
+ public void testTransferBPInTrialWithCTD() throws Exception {
+ final UUID newAccountId = UUID.randomUUID();
+
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+ final DateTime ctd = baseSubscription.getStartDate().plusDays(30);
+
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), ctd, internalCallContext);
+
+ final DateTime evergreenPhaseDate = ((DefaultSubscriptionBase) baseSubscription).getPendingTransition().getEffectiveTransitionTime();
+
+ // MOVE A LITTLE, STILL IN TRIAL
+ clock.addDays(20);
+
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ final DateTime transferRequestedDate = clock.getUTCNow();
+ transferApi.transferBundle(bundle.getAccountId(), newAccountId, bundle.getExternalKey(), transferRequestedDate, false, false, callContext);
+ assertListenerStatus();
+
+ // CHECK OLD BASE IS CANCEL AT THE TRANSFER DATE
+ final SubscriptionBase oldBaseSubscription = subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertNotNull(oldBaseSubscription.getFutureEndDate());
+ assertTrue(oldBaseSubscription.getFutureEndDate().compareTo(ctd) == 0);
+
+ // CHECK NEW BUNDLE EXIST, WITH ONE SUBSCRIPTION STARTING ON TRANSFER_DATE
+ final List<SubscriptionBaseBundle> bundlesForAccountAndKey = subscriptionInternalApi.getBundlesForAccountAndKey(newAccountId, bundle.getExternalKey(), internalCallContext);
+ assertEquals(bundlesForAccountAndKey.size(), 1);
+
+ final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
+
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+
+ final SubscriptionBase newBaseSubscription = subscriptions.get(0);
+ assertTrue(((DefaultSubscriptionBase) newBaseSubscription).getAlignStartDate().compareTo(((DefaultSubscriptionBase) oldBaseSubscription).getAlignStartDate()) == 0);
+
+ // CHECK NEXT PENDING PHASE IS ALIGNED WITH OLD SUBSCRIPTION START DATE
+ assertEquals(subscriptionInternalApi.getAllTransitions(newBaseSubscription, internalCallContext).size(), 2);
+ assertTrue(subscriptionInternalApi.getAllTransitions(newBaseSubscription, internalCallContext).get(1).getEffectiveTransitionTime().compareTo(evergreenPhaseDate) == 0);
+
+ final Plan newPlan = newBaseSubscription.getCurrentPlan();
+ assertEquals(newPlan.getProduct().getName(), baseProduct);
+ assertEquals(newBaseSubscription.getCurrentPhase().getPhaseType(), PhaseType.TRIAL);
+ }
+
+ @Test(groups = "slow")
+ public void testTransferBPNoTrialWithNoCTD() throws Exception {
+ final UUID newAccountId = UUID.randomUUID();
+
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE AFTER TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.addDays(40);
+ assertListenerStatus();
+
+ final DateTime beforeTransferDate = clock.getUTCNow();
+ final DateTime transferRequestedDate = clock.getUTCNow();
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ transferApi.transferBundle(bundle.getAccountId(), newAccountId, bundle.getExternalKey(), transferRequestedDate, false, false, callContext);
+ assertListenerStatus();
+ final DateTime afterTransferDate = clock.getUTCNow();
+
+ // CHECK OLD BASE IS CANCEL AT THE TRANSFER DATE
+ final SubscriptionBase oldBaseSubscription = subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertNotNull(oldBaseSubscription.getEndDate());
+ testUtil.assertDateWithin(oldBaseSubscription.getEndDate(), beforeTransferDate, afterTransferDate);
+ assertTrue(oldBaseSubscription.getEndDate().compareTo(transferRequestedDate) == 0);
+
+ // CHECK NEW BUNDLE EXIST, WITH ONE SUBSCRIPTION STARTING ON TRANSFER_DATE
+ final List<SubscriptionBaseBundle> bundlesForAccountAndKey = subscriptionInternalApi.getBundlesForAccountAndKey(newAccountId, bundle.getExternalKey(), internalCallContext);
+ assertEquals(bundlesForAccountAndKey.size(), 1);
+
+ final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+
+ final SubscriptionBase newBaseSubscription = subscriptions.get(0);
+ assertTrue(((DefaultSubscriptionBase) newBaseSubscription).getAlignStartDate().compareTo(((DefaultSubscriptionBase) baseSubscription).getAlignStartDate()) == 0);
+
+ // CHECK ONLY ONE PHASE EXISTS
+ assertEquals(subscriptionInternalApi.getAllTransitions(newBaseSubscription, internalCallContext).size(), 1);
+
+ final Plan newPlan = newBaseSubscription.getCurrentPlan();
+ assertEquals(newPlan.getProduct().getName(), baseProduct);
+ assertEquals(newBaseSubscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+ }
+
+ @Test(groups = "slow")
+ public void testTransferBPNoTrialWithCTD() throws Exception {
+ final UUID newAccountId = UUID.randomUUID();
+
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE AFTER TRIAL
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.addDays(40);
+ assertListenerStatus();
+
+ // SET CTD
+ final DateTime ctd = baseSubscription.getStartDate().plusDays(30).plusMonths(1);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), ctd, internalCallContext);
+
+ final DateTime transferRequestedDate = clock.getUTCNow();
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ transferApi.transferBundle(bundle.getAccountId(), newAccountId, bundle.getExternalKey(), transferRequestedDate, false, false, callContext);
+ assertListenerStatus();
+
+ // CHECK OLD BASE IS CANCEL AT THE TRANSFER DATE
+ final SubscriptionBase oldBaseSubscription = subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertNotNull(oldBaseSubscription.getFutureEndDate());
+ assertTrue(oldBaseSubscription.getFutureEndDate().compareTo(ctd) == 0);
+
+ // CHECK NEW BUNDLE EXIST, WITH ONE SUBSCRIPTION STARTING ON TRANSFER_DATE
+ final List<SubscriptionBaseBundle> bundlesForAccountAndKey = subscriptionInternalApi.getBundlesForAccountAndKey(newAccountId, bundle.getExternalKey(), internalCallContext);
+ assertEquals(bundlesForAccountAndKey.size(), 1);
+
+ final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+
+ final SubscriptionBase newBaseSubscription = subscriptions.get(0);
+ assertTrue(((DefaultSubscriptionBase) newBaseSubscription).getAlignStartDate().compareTo(((DefaultSubscriptionBase) baseSubscription).getAlignStartDate()) == 0);
+
+ // CHECK ONLY ONE PHASE EXISTS
+ assertEquals(subscriptionInternalApi.getAllTransitions(newBaseSubscription, internalCallContext).size(), 1);
+
+ Plan newPlan = newBaseSubscription.getCurrentPlan();
+ assertEquals(newPlan.getProduct().getName(), baseProduct);
+ assertEquals(newBaseSubscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+
+ // MAKE A PLAN CHANGE IMM
+ clock.addDays(5);
+
+ final String newBaseProduct1 = "Assault-Rifle";
+ final BillingPeriod newBaseTerm1 = BillingPeriod.ANNUAL;
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ newBaseSubscription.changePlan(newBaseProduct1, newBaseTerm1, basePriceList, callContext);
+ assertListenerStatus();
+
+ newPlan = newBaseSubscription.getCurrentPlan();
+ assertEquals(newPlan.getProduct().getName(), newBaseProduct1);
+ assertEquals(newBaseSubscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+
+ // SET CTD AND MAKE CHANGE EOT
+ clock.addDays(2);
+
+ final DateTime newCtd = newBaseSubscription.getStartDate().plusYears(1);
+ subscriptionInternalApi.setChargedThroughDate(newBaseSubscription.getId(), newCtd, internalCallContext);
+ final SubscriptionBase newBaseSubscriptionWithCtd = subscriptionInternalApi.getSubscriptionFromId(newBaseSubscription.getId(), internalCallContext);
+
+ final String newBaseProduct2 = "Pistol";
+ final BillingPeriod newBaseTerm2 = BillingPeriod.ANNUAL;
+ newBaseSubscriptionWithCtd.changePlan(newBaseProduct2, newBaseTerm2, basePriceList, callContext);
+
+ newPlan = newBaseSubscriptionWithCtd.getCurrentPlan();
+ assertEquals(newPlan.getProduct().getName(), newBaseProduct1);
+ assertEquals(newBaseSubscriptionWithCtd.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+
+ assertNotNull(newBaseSubscriptionWithCtd.getPendingTransition());
+ assertEquals(newBaseSubscriptionWithCtd.getPendingTransition().getEffectiveTransitionTime(), newCtd);
+ }
+
+ @Test(groups = "slow")
+ public void testTransferWithAO() throws Exception {
+ final UUID newAccountId = UUID.randomUUID();
+
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE 3 DAYS AND CREATE AO1
+ clock.addDays(3);
+ final String aoProduct1 = "Telescopic-Scope";
+ final BillingPeriod aoTerm1 = BillingPeriod.MONTHLY;
+ final DefaultSubscriptionBase aoSubscription1 = testUtil.createSubscription(bundle, aoProduct1, aoTerm1, basePriceList);
+ assertEquals(aoSubscription1.getState(), EntitlementState.ACTIVE);
+
+ // MOVE ANOTHER 25 DAYS AND CREATE AO2 [ BP STILL IN TRIAL]
+ // LASER-SCOPE IS SUBSCRIPTION ALIGN SO EVERGREN WILL ONLY START IN A MONTH
+ clock.addDays(25);
+ final String aoProduct2 = "Laser-Scope";
+ final BillingPeriod aoTerm2 = BillingPeriod.MONTHLY;
+ final DefaultSubscriptionBase aoSubscription2 = testUtil.createSubscription(bundle, aoProduct2, aoTerm2, basePriceList);
+ assertEquals(aoSubscription2.getState(), EntitlementState.ACTIVE);
+
+ // MOVE AFTER TRIAL AND AO DISCOUNT PHASE [LASER SCOPE STILL IN DISCOUNT]
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.addDays(5);
+ assertListenerStatus();
+
+ // SET CTD TO TRIGGER CANCELLATION EOT
+ final DateTime ctd = baseSubscription.getStartDate().plusDays(30).plusMonths(1);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), ctd, internalCallContext);
+
+ final DateTime transferRequestedDate = clock.getUTCNow();
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ transferApi.transferBundle(bundle.getAccountId(), newAccountId, bundle.getExternalKey(), transferRequestedDate, true, false, callContext);
+ assertListenerStatus();
+
+ // RETRIEVE NEW BUNDLE AND CHECK SUBSCRIPTIONS
+ final List<SubscriptionBaseBundle> bundlesForAccountAndKey = subscriptionInternalApi.getBundlesForAccountAndKey(newAccountId, bundle.getExternalKey(), internalCallContext);
+ assertEquals(bundlesForAccountAndKey.size(), 1);
+
+ final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 3);
+ boolean foundBP = false;
+ boolean foundAO1 = false;
+ boolean foundAO2 = false;
+ for (final SubscriptionBase cur : subscriptions) {
+ final Plan curPlan = cur.getCurrentPlan();
+ final Product curProduct = curPlan.getProduct();
+ if (curProduct.getName().equals(baseProduct)) {
+ foundBP = true;
+ assertTrue(((DefaultSubscriptionBase) cur).getAlignStartDate().compareTo(((DefaultSubscriptionBase) baseSubscription).getAlignStartDate()) == 0);
+ assertNull(cur.getPendingTransition());
+ } else if (curProduct.getName().equals(aoProduct1)) {
+ foundAO1 = true;
+ assertTrue(((DefaultSubscriptionBase) cur).getAlignStartDate().compareTo((aoSubscription1).getAlignStartDate()) == 0);
+ assertNull(cur.getPendingTransition());
+ } else if (curProduct.getName().equals(aoProduct2)) {
+ foundAO2 = true;
+ assertTrue(((DefaultSubscriptionBase) cur).getAlignStartDate().compareTo((aoSubscription2).getAlignStartDate()) == 0);
+ assertNotNull(cur.getPendingTransition());
+ } else {
+ Assert.fail("Unexpected product " + curProduct.getName());
+ }
+ }
+ assertTrue(foundBP);
+ assertTrue(foundAO1);
+ assertTrue(foundAO2);
+
+ // MOVE AFTER CANCEL DATE TO TRIGGER OLD SUBSCRIPTIONS CANCELLATION + LASER_SCOPE PHASE EVENT
+ testListener.pushExpectedEvents(NextEvent.PHASE, NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ // ISSUE ANOTHER TRANSFER TO CHECK THAT WE CAN TRANSFER AGAIN-- NOTE WILL NOT WORK ON PREVIOUS ACCOUNT (LIMITATION)
+ final UUID finalNewAccountId = UUID.randomUUID();
+ final DateTime newTransferRequestedDate = clock.getUTCNow();
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ transferApi.transferBundle(newBundle.getAccountId(), finalNewAccountId, newBundle.getExternalKey(), newTransferRequestedDate, true, false, callContext);
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testTransferWithAOCancelled() throws Exception {
+ final UUID newAccountId = UUID.randomUUID();
+
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE 3 DAYS AND CREATE AO1
+ clock.addDays(3);
+ final String aoProduct1 = "Telescopic-Scope";
+ final BillingPeriod aoTerm1 = BillingPeriod.MONTHLY;
+ final DefaultSubscriptionBase aoSubscription1 = testUtil.createSubscription(bundle, aoProduct1, aoTerm1, basePriceList);
+ assertEquals(aoSubscription1.getState(), EntitlementState.ACTIVE);
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ // SET CTD TO TRIGGER CANCELLATION EOT
+ final DateTime ctd = baseSubscription.getStartDate().plusDays(30).plusMonths(1);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), ctd, internalCallContext);
+
+ // SET CTD TO TRIGGER CANCELLATION EOT
+ subscriptionInternalApi.setChargedThroughDate(aoSubscription1.getId(), ctd, internalCallContext);
+
+ // CANCEL ADDON
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ aoSubscription1.cancelWithDate(clock.getUTCNow(), callContext);
+ assertListenerStatus();
+
+ clock.addDays(1);
+
+ final DateTime transferRequestedDate = clock.getUTCNow();
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ transferApi.transferBundle(bundle.getAccountId(), newAccountId, bundle.getExternalKey(), transferRequestedDate, true, false, callContext);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseBundle> bundlesForAccountAndKey = subscriptionInternalApi.getBundlesForAccountAndKey(newAccountId, bundle.getExternalKey(), internalCallContext);
+ assertEquals(bundlesForAccountAndKey.size(), 1);
+
+ final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+ }
+
+ @Test(groups = "slow")
+ public void testTransferWithUncancel() throws Exception {
+ final UUID newAccountId = UUID.randomUUID();
+
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ SubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ // SET CTD TO TRIGGER CANCELLATION EOT
+ final DateTime ctd = baseSubscription.getStartDate().plusDays(30).plusMonths(1);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), ctd, internalCallContext);
+
+ // CANCEL BP
+ baseSubscription = subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ baseSubscription.cancel(callContext);
+
+ // MOVE CLOCK one day AHEAD AND UNCANCEL BP
+ clock.addDays(1);
+ testListener.pushExpectedEvent(NextEvent.UNCANCEL);
+ baseSubscription.uncancel(callContext);
+ assertListenerStatus();
+
+ // MOVE CLOCK one day AHEAD AND UNCANCEL BP
+ clock.addDays(1);
+ final DateTime transferRequestedDate = clock.getUTCNow();
+ testListener.pushExpectedEvent(NextEvent.TRANSFER);
+ transferApi.transferBundle(bundle.getAccountId(), newAccountId, bundle.getExternalKey(), transferRequestedDate, true, false, callContext);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseBundle> bundlesForAccountAndKey = subscriptionInternalApi.getBundlesForAccountAndKey(newAccountId, bundle.getExternalKey(), internalCallContext);
+ assertEquals(bundlesForAccountAndKey.size(), 1);
+
+ final SubscriptionBaseBundle newBundle = bundlesForAccountAndKey.get(0);
+ final List<SubscriptionBase> subscriptions = subscriptionInternalApi.getSubscriptionsForBundle(newBundle.getId(), internalCallContext);
+ assertEquals(subscriptions.size(), 1);
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestSubscriptionHelper.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestSubscriptionHelper.java
new file mode 100644
index 0000000..93b17b2
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestSubscriptionHelper.java
@@ -0,0 +1,677 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+import org.joda.time.Period;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Duration;
+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.catalog.api.TimeUnit;
+import org.killbill.clock.Clock;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.AccountMigration;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.BundleMigration;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.SubscriptionMigration;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi.SubscriptionMigrationCase;
+import org.killbill.billing.subscription.api.timeline.BundleBaseTimeline;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseRepairException;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.DeletedEvent;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.ExistingEvent;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimeline.NewEvent;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.phase.PhaseEvent;
+import org.killbill.billing.subscription.events.user.ApiEvent;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestSubscriptionHelper {
+
+ private final Logger log = LoggerFactory.getLogger(TestSubscriptionHelper.class);
+
+ private final SubscriptionBaseInternalApi subscriptionApi;
+
+ private final Clock clock;
+
+ private final InternalCallContext callContext;
+
+ private final TestApiListener testListener;
+
+ private final SubscriptionDao dao;
+
+ @Inject
+ public TestSubscriptionHelper(final SubscriptionBaseInternalApi subscriptionApi, final Clock clock, final InternalCallContext callContext, final TestApiListener testListener, final SubscriptionDao dao) {
+ this.subscriptionApi = subscriptionApi;
+ this.clock = clock;
+ this.callContext = callContext;
+ this.testListener = testListener;
+ this.dao = dao;
+ }
+
+ public DefaultSubscriptionBase createSubscription(final SubscriptionBaseBundle bundle, final String productName, final BillingPeriod term, final String planSet, final DateTime requestedDate)
+ throws SubscriptionBaseApiException {
+ return createSubscriptionWithBundle(bundle.getId(), productName, term, planSet, requestedDate);
+ }
+
+ public DefaultSubscriptionBase createSubscription(final SubscriptionBaseBundle bundle, final String productName, final BillingPeriod term, final String planSet)
+ throws SubscriptionBaseApiException {
+ return createSubscriptionWithBundle(bundle.getId(), productName, term, planSet, null);
+ }
+
+ public DefaultSubscriptionBase createSubscriptionWithBundle(final UUID bundleId, final String productName, final BillingPeriod term, final String planSet, final DateTime requestedDate)
+ throws SubscriptionBaseApiException {
+ testListener.pushExpectedEvent(NextEvent.CREATE);
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionApi.createSubscription(bundleId,
+ new PlanPhaseSpecifier(productName, ProductCategory.BASE, term, planSet, null),
+ requestedDate == null ? clock.getUTCNow() : requestedDate, callContext);
+ assertNotNull(subscription);
+
+ testListener.assertListenerStatus();
+
+ return subscription;
+ }
+
+ public void checkNextPhaseChange(final DefaultSubscriptionBase subscription, final int expPendingEvents, final DateTime expPhaseChange) {
+ final List<SubscriptionBaseEvent> events = dao.getPendingEventsForSubscription(subscription.getId(), callContext);
+ assertNotNull(events);
+ printEvents(events);
+ assertEquals(events.size(), expPendingEvents);
+ if (events.size() > 0 && expPhaseChange != null) {
+ boolean foundPhase = false;
+ boolean foundChange = false;
+
+ for (final SubscriptionBaseEvent cur : events) {
+ if (cur instanceof PhaseEvent) {
+ assertEquals(foundPhase, false);
+ foundPhase = true;
+ assertEquals(cur.getEffectiveDate(), expPhaseChange);
+ } else if (cur instanceof ApiEvent) {
+ final ApiEvent uEvent = (ApiEvent) cur;
+ assertEquals(ApiEventType.CHANGE, uEvent.getEventType());
+ assertEquals(foundChange, false);
+ foundChange = true;
+ } else {
+ assertFalse(true);
+ }
+ }
+ }
+ }
+
+ public void assertDateWithin(final DateTime in, final DateTime lower, final DateTime upper) {
+ assertTrue(in.isEqual(lower) || in.isAfter(lower));
+ assertTrue(in.isEqual(upper) || in.isBefore(upper));
+ }
+
+ public Duration getDurationDay(final int days) {
+ final Duration result = new Duration() {
+ @Override
+ public TimeUnit getUnit() {
+ return TimeUnit.DAYS;
+ }
+
+ @Override
+ public int getNumber() {
+ return days;
+ }
+
+ @Override
+ public DateTime addToDateTime(final DateTime dateTime) {
+ return null;
+ }
+
+ @Override
+ public Period toJodaPeriod() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ return result;
+ }
+
+ public Duration getDurationMonth(final int months) {
+ final Duration result = new Duration() {
+ @Override
+ public TimeUnit getUnit() {
+ return TimeUnit.MONTHS;
+ }
+
+ @Override
+ public int getNumber() {
+ return months;
+ }
+
+ @Override
+ public DateTime addToDateTime(final DateTime dateTime) {
+ return null; //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public Period toJodaPeriod() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ return result;
+ }
+
+ public Duration getDurationYear(final int years) {
+ final Duration result = new Duration() {
+ @Override
+ public TimeUnit getUnit() {
+ return TimeUnit.YEARS;
+ }
+
+ @Override
+ public int getNumber() {
+ return years;
+ }
+
+ @Override
+ public DateTime addToDateTime(final DateTime dateTime) {
+ return dateTime.plusYears(years);
+ }
+
+ @Override
+ public Period toJodaPeriod() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ return result;
+ }
+
+ public PlanPhaseSpecifier getProductSpecifier(final String productName, final String priceList,
+ final BillingPeriod term,
+ @Nullable final PhaseType phaseType) {
+ return new PlanPhaseSpecifier(productName, ProductCategory.BASE, term, priceList, phaseType);
+ }
+
+ public void printEvents(final List<SubscriptionBaseEvent> events) {
+ for (final SubscriptionBaseEvent cur : events) {
+ log.debug("Inspect event " + cur);
+ }
+ }
+
+ public void printSubscriptionTransitions(final List<EffectiveSubscriptionInternalEvent> transitions) {
+ for (final EffectiveSubscriptionInternalEvent cur : transitions) {
+ log.debug("Transition " + cur);
+ }
+ }
+
+ /**
+ * ***********************************************************
+ * Utilities for migration tests
+ * *************************************************************
+ */
+
+ public AccountMigration createAccountForMigrationTest(final List<List<SubscriptionMigrationCaseWithCTD>> cases) {
+ return new AccountMigration() {
+ private final UUID accountId = UUID.randomUUID();
+
+ @Override
+ public BundleMigration[] getBundles() {
+ final List<BundleMigration> bundles = new ArrayList<BundleMigration>();
+ final BundleMigration bundle0 = new BundleMigration() {
+ @Override
+ public SubscriptionMigration[] getSubscriptions() {
+ final SubscriptionMigration[] result = new SubscriptionMigration[cases.size()];
+
+ for (int i = 0; i < cases.size(); i++) {
+ final List<SubscriptionMigrationCaseWithCTD> curCases = cases.get(i);
+ final SubscriptionMigration subscription = new SubscriptionMigration() {
+ @Override
+ public SubscriptionMigrationCaseWithCTD[] getSubscriptionCases() {
+ return curCases.toArray(new SubscriptionMigrationCaseWithCTD[curCases.size()]);
+ }
+
+ @Override
+ public ProductCategory getCategory() {
+ return curCases.get(0).getPlanPhaseSpecifier().getProductCategory();
+ }
+
+ @Override
+ public DateTime getChargedThroughDate() {
+ for (final SubscriptionMigrationCaseWithCTD cur : curCases) {
+ if (cur.getChargedThroughDate() != null) {
+ return cur.getChargedThroughDate();
+ }
+ }
+ return null;
+ }
+ };
+ result[i] = subscription;
+ }
+ return result;
+ }
+
+ @Override
+ public String getBundleKey() {
+ return "12345";
+ }
+ };
+ bundles.add(bundle0);
+ return bundles.toArray(new BundleMigration[bundles.size()]);
+ }
+
+ @Override
+ public UUID getAccountKey() {
+ return accountId;
+ }
+ };
+ }
+
+ public AccountMigration createAccountForMigrationWithRegularBasePlanAndAddons(final DateTime initialBPstart, final DateTime initalAddonStart) {
+
+ final List<SubscriptionMigrationCaseWithCTD> cases = new LinkedList<SubscriptionMigrationCaseWithCTD>();
+ cases.add(new SubscriptionMigrationCaseWithCTD(
+ new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN),
+ initialBPstart,
+ null,
+ initialBPstart.plusYears(1)));
+
+ final List<SubscriptionMigrationCaseWithCTD> firstAddOnCases = new LinkedList<SubscriptionMigrationCaseWithCTD>();
+ firstAddOnCases.add(new SubscriptionMigrationCaseWithCTD(
+ new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.DISCOUNT),
+ initalAddonStart,
+ initalAddonStart.plusMonths(1),
+ initalAddonStart.plusMonths(1)));
+ firstAddOnCases.add(new SubscriptionMigrationCaseWithCTD(
+ new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN),
+ initalAddonStart.plusMonths(1),
+ null,
+ null));
+
+ final List<List<SubscriptionMigrationCaseWithCTD>> input = new ArrayList<List<SubscriptionMigrationCaseWithCTD>>();
+ input.add(cases);
+ input.add(firstAddOnCases);
+ return createAccountForMigrationTest(input);
+ }
+
+ public AccountMigration createAccountForMigrationWithRegularBasePlan(final DateTime startDate) {
+ final List<SubscriptionMigrationCaseWithCTD> cases = new LinkedList<SubscriptionMigrationCaseWithCTD>();
+ cases.add(new SubscriptionMigrationCaseWithCTD(
+ new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN),
+ startDate,
+ null,
+ startDate.plusYears(1)));
+ final List<List<SubscriptionMigrationCaseWithCTD>> input = new ArrayList<List<SubscriptionMigrationCaseWithCTD>>();
+ input.add(cases);
+ return createAccountForMigrationTest(input);
+ }
+
+ public AccountMigration createAccountForMigrationWithRegularBasePlanFutreCancelled(final DateTime startDate) {
+ final List<SubscriptionMigrationCaseWithCTD> cases = new LinkedList<SubscriptionMigrationCaseWithCTD>();
+ cases.add(new SubscriptionMigrationCaseWithCTD(
+ new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN),
+ startDate,
+ startDate.plusYears(1),
+ startDate.plusYears(1)));
+ final List<List<SubscriptionMigrationCaseWithCTD>> input = new ArrayList<List<SubscriptionMigrationCaseWithCTD>>();
+ input.add(cases);
+ return createAccountForMigrationTest(input);
+ }
+
+ public AccountMigration createAccountForMigrationFuturePendingPhase(final DateTime trialDate) {
+ final List<SubscriptionMigrationCaseWithCTD> cases = new LinkedList<SubscriptionMigrationCaseWithCTD>();
+ cases.add(new SubscriptionMigrationCaseWithCTD(
+ new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL),
+ trialDate,
+ trialDate.plusDays(30),
+ trialDate.plusDays(30)));
+ cases.add(new SubscriptionMigrationCaseWithCTD(
+ new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN),
+ trialDate.plusDays(30),
+ null,
+ null));
+ final List<List<SubscriptionMigrationCaseWithCTD>> input = new ArrayList<List<SubscriptionMigrationCaseWithCTD>>();
+ input.add(cases);
+ return createAccountForMigrationTest(input);
+ }
+
+ public AccountMigration createAccountForMigrationFuturePendingChange() {
+ final List<SubscriptionMigrationCaseWithCTD> cases = new LinkedList<SubscriptionMigrationCaseWithCTD>();
+ final DateTime effectiveDate = clock.getUTCNow().minusDays(10);
+ cases.add(new SubscriptionMigrationCaseWithCTD(
+ new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN),
+ effectiveDate,
+ effectiveDate.plusMonths(1),
+ effectiveDate.plusMonths(1)));
+ cases.add(new SubscriptionMigrationCaseWithCTD(
+ new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN),
+ effectiveDate.plusMonths(1).plusDays(1),
+ null,
+ null));
+ final List<List<SubscriptionMigrationCaseWithCTD>> input = new ArrayList<List<SubscriptionMigrationCaseWithCTD>>();
+ input.add(cases);
+ return createAccountForMigrationTest(input);
+ }
+
+ public SubscriptionBaseTimeline createSubscriptionRepair(final UUID id, final List<DeletedEvent> deletedEvents, final List<NewEvent> newEvents) {
+ return new SubscriptionBaseTimeline() {
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return null;
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return null;
+ }
+
+ @Override
+ public List<NewEvent> getNewEvents() {
+ return newEvents;
+ }
+
+ @Override
+ public List<ExistingEvent> getExistingEvents() {
+ return null;
+ }
+
+ @Override
+ public List<DeletedEvent> getDeletedEvents() {
+ return deletedEvents;
+ }
+
+ @Override
+ public long getActiveVersion() {
+ return 1;
+ }
+ };
+ }
+
+ public BundleBaseTimeline createBundleRepair(final UUID bundleId, final String viewId, final List<SubscriptionBaseTimeline> subscriptionRepair) {
+ return new BundleBaseTimeline() {
+ @Override
+ public String getViewId() {
+ return viewId;
+ }
+
+ @Override
+ public List<SubscriptionBaseTimeline> getSubscriptions() {
+ return subscriptionRepair;
+ }
+
+ @Override
+ public UUID getId() {
+ return bundleId;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return null;
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return null;
+ }
+
+ @Override
+ public String getExternalKey() {
+ return null;
+ }
+ };
+ }
+
+ public ExistingEvent createExistingEventForAssertion(final SubscriptionBaseTransitionType type,
+ final String productName, final PhaseType phaseType, final ProductCategory category, final String priceListName, final BillingPeriod billingPeriod,
+ final DateTime effectiveDateTime) {
+ final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(productName, category, billingPeriod, priceListName, phaseType);
+ return new ExistingEvent() {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return type;
+ }
+
+ @Override
+ public DateTime getRequestedDate() {
+ return null;
+ }
+
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return spec;
+ }
+
+ @Override
+ public UUID getEventId() {
+ return null;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effectiveDateTime;
+ }
+
+ @Override
+ public String getPlanPhaseName() {
+ return null;
+ }
+ };
+ }
+
+ public SubscriptionBaseTimeline getSubscriptionRepair(final UUID id, final BundleBaseTimeline bundleRepair) {
+ for (final SubscriptionBaseTimeline cur : bundleRepair.getSubscriptions()) {
+ if (cur.getId().equals(id)) {
+ return cur;
+ }
+ }
+ Assert.fail("Failed to find SubscriptionRepair " + id);
+ return null;
+ }
+
+ public void validateExistingEventForAssertion(final ExistingEvent expected, final ExistingEvent input) {
+ log.debug(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getProductName(), expected.getPlanPhaseSpecifier().getProductName()));
+ assertEquals(input.getPlanPhaseSpecifier().getProductName(), expected.getPlanPhaseSpecifier().getProductName());
+ log.debug(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getPhaseType(), expected.getPlanPhaseSpecifier().getPhaseType()));
+ assertEquals(input.getPlanPhaseSpecifier().getPhaseType(), expected.getPlanPhaseSpecifier().getPhaseType());
+ log.debug(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getProductCategory(), expected.getPlanPhaseSpecifier().getProductCategory()));
+ assertEquals(input.getPlanPhaseSpecifier().getProductCategory(), expected.getPlanPhaseSpecifier().getProductCategory());
+ log.debug(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getPriceListName(), expected.getPlanPhaseSpecifier().getPriceListName()));
+ assertEquals(input.getPlanPhaseSpecifier().getPriceListName(), expected.getPlanPhaseSpecifier().getPriceListName());
+ log.debug(String.format("Got %s -> Expected %s", input.getPlanPhaseSpecifier().getBillingPeriod(), expected.getPlanPhaseSpecifier().getBillingPeriod()));
+ assertEquals(input.getPlanPhaseSpecifier().getBillingPeriod(), expected.getPlanPhaseSpecifier().getBillingPeriod());
+ log.debug(String.format("Got %s -> Expected %s", input.getEffectiveDate(), expected.getEffectiveDate()));
+ assertEquals(input.getEffectiveDate(), expected.getEffectiveDate());
+ }
+
+ public DeletedEvent createDeletedEvent(final UUID eventId) {
+ return new DeletedEvent() {
+ @Override
+ public UUID getEventId() {
+ return eventId;
+ }
+ };
+ }
+
+ public NewEvent createNewEvent(final SubscriptionBaseTransitionType type, final DateTime requestedDate, final PlanPhaseSpecifier spec) {
+ return new NewEvent() {
+ @Override
+ public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+ return type;
+ }
+
+ @Override
+ public DateTime getRequestedDate() {
+ return requestedDate;
+ }
+
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return spec;
+ }
+ };
+ }
+
+ public void sortEventsOnBundle(final BundleBaseTimeline bundle) {
+ if (bundle.getSubscriptions() == null) {
+ return;
+ }
+ for (final SubscriptionBaseTimeline cur : bundle.getSubscriptions()) {
+ if (cur.getExistingEvents() != null) {
+ sortExistingEvent(cur.getExistingEvents());
+ }
+ if (cur.getNewEvents() != null) {
+ sortNewEvent(cur.getNewEvents());
+ }
+ }
+ }
+
+ public void sortExistingEvent(final List<ExistingEvent> events) {
+ Collections.sort(events, new Comparator<ExistingEvent>() {
+ @Override
+ public int compare(final ExistingEvent arg0, final ExistingEvent arg1) {
+ return arg0.getEffectiveDate().compareTo(arg1.getEffectiveDate());
+ }
+ });
+ }
+
+ public void sortNewEvent(final List<NewEvent> events) {
+ Collections.sort(events, new Comparator<NewEvent>() {
+ @Override
+ public int compare(final NewEvent arg0, final NewEvent arg1) {
+ return arg0.getRequestedDate().compareTo(arg1.getRequestedDate());
+ }
+ });
+ }
+
+ public static DateTime addOrRemoveDuration(final DateTime input, final List<Duration> durations, final boolean add) {
+ DateTime result = input;
+ for (final Duration cur : durations) {
+ switch (cur.getUnit()) {
+ case DAYS:
+ result = add ? result.plusDays(cur.getNumber()) : result.minusDays(cur.getNumber());
+ break;
+
+ case MONTHS:
+ result = add ? result.plusMonths(cur.getNumber()) : result.minusMonths(cur.getNumber());
+ break;
+
+ case YEARS:
+ result = add ? result.plusYears(cur.getNumber()) : result.minusYears(cur.getNumber());
+ break;
+ case UNLIMITED:
+ default:
+ throw new RuntimeException("Trying to move to unlimited time period");
+ }
+ }
+ return result;
+ }
+
+ public static DateTime addDuration(final DateTime input, final List<Duration> durations) {
+ return addOrRemoveDuration(input, durations, true);
+ }
+
+ public static DateTime removeDuration(final DateTime input, final List<Duration> durations) {
+ return addOrRemoveDuration(input, durations, false);
+ }
+
+ public static DateTime addDuration(final DateTime input, final Duration duration) {
+ final List<Duration> list = new ArrayList<Duration>();
+ list.add(duration);
+ return addOrRemoveDuration(input, list, true);
+ }
+
+ public static DateTime removeDuration(final DateTime input, final Duration duration) {
+ final List<Duration> list = new ArrayList<Duration>();
+ list.add(duration);
+ return addOrRemoveDuration(input, list, false);
+ }
+
+ public static class SubscriptionMigrationCaseWithCTD implements SubscriptionMigrationCase {
+
+ private final PlanPhaseSpecifier pps;
+ private final DateTime effDt;
+ private final DateTime cancelDt;
+ private final DateTime ctd;
+
+ public SubscriptionMigrationCaseWithCTD(final PlanPhaseSpecifier pps, final DateTime effDt, final DateTime cancelDt, final DateTime ctd) {
+ this.pps = pps;
+ this.cancelDt = cancelDt;
+ this.effDt = effDt;
+ this.ctd = ctd;
+ }
+
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+ return pps;
+ }
+
+ @Override
+ public DateTime getEffectiveDate() {
+ return effDt;
+ }
+
+ @Override
+ public DateTime getCancelledDate() {
+ return cancelDt;
+ }
+
+ public DateTime getChargedThroughDate() {
+ return ctd;
+ }
+ }
+
+ public interface TestWithExceptionCallback {
+
+ public void doTest() throws SubscriptionBaseRepairException, SubscriptionBaseApiException;
+ }
+
+ public static class TestWithException {
+
+ public void withException(final TestWithExceptionCallback callback, final ErrorCode code) throws Exception {
+ try {
+ callback.doTest();
+ Assert.fail("Failed to catch exception " + code);
+ } catch (SubscriptionBaseRepairException e) {
+ assertEquals(e.getCode(), code.getCode());
+ }
+ }
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
new file mode 100644
index 0000000..aa8003e
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
@@ -0,0 +1,492 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+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;
+import org.killbill.billing.catalog.api.Duration;
+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.PlanSpecifier;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testCreateCancelAddon() throws SubscriptionBaseApiException {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.ANNUAL;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ final String aoProduct = "Telescopic-Scope";
+ final BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList);
+ assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
+
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ final DateTime now = clock.getUTCNow();
+ aoSubscription.cancel(callContext);
+
+ assertListenerStatus();
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(aoSubscription.getState(), EntitlementState.CANCELLED);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testCreateCancelAddonAndThenBP() throws SubscriptionBaseApiException {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.ANNUAL;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ final String aoProduct = "Telescopic-Scope";
+ final BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList);
+ assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
+
+ // Move clock after a month
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(1));
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // SET CTD TO CANCEL IN FUTURE
+ final DateTime now = clock.getUTCNow();
+ final Duration aoCtd = testUtil.getDurationMonth(1);
+ final DateTime newAOChargedThroughDate = TestSubscriptionHelper.addDuration(now, aoCtd);
+ subscriptionInternalApi.setChargedThroughDate(aoSubscription.getId(), newAOChargedThroughDate, internalCallContext);
+
+ final Duration bpCtd = testUtil.getDurationMonth(11);
+ final DateTime newBPChargedThroughDate = TestSubscriptionHelper.addDuration(now, bpCtd);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newBPChargedThroughDate, internalCallContext);
+
+ baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ // CANCEL AO
+ aoSubscription.cancel(callContext);
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
+ assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+
+ // CANCEL BASE NOW
+ baseSubscription.cancel(callContext);
+ baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(baseSubscription.getState(), EntitlementState.ACTIVE);
+ assertTrue(baseSubscription.isSubscriptionFutureCancelled());
+
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ List<SubscriptionBaseTransition> aoTransitions = aoSubscription.getAllTransitions();
+ assertEquals(aoTransitions.size(), 3);
+ assertEquals(aoTransitions.get(0).getTransitionType(), SubscriptionBaseTransitionType.CREATE);
+ assertEquals(aoTransitions.get(1).getTransitionType(), SubscriptionBaseTransitionType.PHASE);
+ assertEquals(aoTransitions.get(2).getTransitionType(), SubscriptionBaseTransitionType.CANCEL);
+ assertTrue(aoSubscription.getFutureEndDate().compareTo(newAOChargedThroughDate) == 0);
+
+ testListener.pushExpectedEvent(NextEvent.UNCANCEL);
+ aoSubscription.uncancel(callContext);
+ assertListenerStatus();
+
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ aoTransitions = aoSubscription.getAllTransitions();
+ assertEquals(aoTransitions.size(), 3);
+ assertEquals(aoTransitions.get(0).getTransitionType(), SubscriptionBaseTransitionType.CREATE);
+ assertEquals(aoTransitions.get(1).getTransitionType(), SubscriptionBaseTransitionType.PHASE);
+ assertEquals(aoTransitions.get(2).getTransitionType(), SubscriptionBaseTransitionType.CANCEL);
+ assertTrue(aoSubscription.getFutureEndDate().compareTo(newBPChargedThroughDate) == 0);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testCancelBPWithAddon() throws SubscriptionBaseApiException {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ final String aoProduct = "Telescopic-Scope";
+ final BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList);
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ // MOVE CLOCK AFTER TRIAL + AO DISCOUNT
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(2));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // SET CTD TO CANCEL IN FUTURE
+ final DateTime now = clock.getUTCNow();
+ final Duration ctd = testUtil.getDurationMonth(1);
+ // Why not just use clock.getUTCNow().plusMonths(1) ?
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(now, ctd);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate, internalCallContext);
+ baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ // FUTURE CANCELLATION
+ baseSubscription.cancel(callContext);
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
+ assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+
+ // MOVE AFTER CANCELLATION
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS IS CANCELLED
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(aoSubscription.getState(), EntitlementState.CANCELLED);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testCancelUncancelBPWithAddon() throws SubscriptionBaseApiException {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ final String aoProduct = "Telescopic-Scope";
+ final BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList);
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ // MOVE CLOCK AFTER TRIAL + AO DISCOUNT
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(2));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // SET CTD TO CANCEL IN FUTURE
+ final DateTime now = clock.getUTCNow();
+ final Duration ctd = testUtil.getDurationMonth(1);
+ // Why not just use clock.getUTCNow().plusMonths(1) ?
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(now, ctd);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate, internalCallContext);
+ baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ // FUTURE CANCELLATION
+ baseSubscription.cancel(callContext);
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
+ assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+
+ testListener.pushExpectedEvent(NextEvent.UNCANCEL);
+ baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ baseSubscription.uncancel(callContext);
+ assertListenerStatus();
+
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
+ assertFalse(aoSubscription.isSubscriptionFutureCancelled());
+
+ // CANCEL AGAIN
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ baseSubscription.cancel(callContext);
+ baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+ assertEquals(baseSubscription.getState(), EntitlementState.ACTIVE);
+ assertTrue(baseSubscription.isSubscriptionFutureCancelled());
+
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
+ assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testChangeBPWithAddonIncluded() throws SubscriptionBaseApiException {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ final String aoProduct = "Telescopic-Scope";
+ final BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList);
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ // MOVE CLOCK AFTER TRIAL + AO DISCOUNT
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(2));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // SET CTD TO CHANGE IN FUTURE
+ final DateTime now = clock.getUTCNow();
+ final Duration ctd = testUtil.getDurationMonth(1);
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(now, ctd);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate, internalCallContext);
+ baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ // CHANGE IMMEDIATELY WITH TO BP WITH NON INCLUDED ADDON
+ final String newBaseProduct = "Assault-Rifle";
+ final BillingPeriod newBaseTerm = BillingPeriod.MONTHLY;
+ final String newBasePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ final List<EntitlementAOStatusDryRun> aoStatus = subscriptionInternalApi.getDryRunChangePlanStatus(baseSubscription.getId(),
+ newBaseProduct, now, internalCallContext);
+ assertEquals(aoStatus.size(), 1);
+ assertEquals(aoStatus.get(0).getId(), aoSubscription.getId());
+ assertEquals(aoStatus.get(0).getProductName(), aoProduct);
+ assertEquals(aoStatus.get(0).getBillingPeriod(), aoTerm);
+ assertEquals(aoStatus.get(0).getPhaseType(), aoSubscription.getCurrentPhase().getPhaseType());
+ assertEquals(aoStatus.get(0).getPriceList(), aoSubscription.getCurrentPriceList().getName());
+ assertEquals(aoStatus.get(0).getReason(), DryRunChangeReason.AO_INCLUDED_IN_NEW_PLAN);
+
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ baseSubscription.changePlan(newBaseProduct, newBaseTerm, newBasePriceList, callContext);
+ assertListenerStatus();
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS CANCELLED
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(aoSubscription.getState(), EntitlementState.CANCELLED);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testChangeBPWithAddonNonAvailable() throws SubscriptionBaseApiException {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ final String aoProduct = "Telescopic-Scope";
+ final BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE AO
+ DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList);
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ // MOVE CLOCK AFTER TRIAL + AO DISCOUNT
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(2));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // SET CTD TO CANCEL IN FUTURE
+ final DateTime now = clock.getUTCNow();
+ final Duration ctd = testUtil.getDurationMonth(1);
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(now, ctd);
+ subscriptionInternalApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate, internalCallContext);
+ baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
+
+ // CHANGE IMMEDIATELY WITH TO BP WITH NON AVAILABLE ADDON
+ final String newBaseProduct = "Pistol";
+ final BillingPeriod newBaseTerm = BillingPeriod.MONTHLY;
+ final String newBasePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ final List<EntitlementAOStatusDryRun> aoStatus = subscriptionInternalApi.getDryRunChangePlanStatus(baseSubscription.getId(),
+ newBaseProduct, now, internalCallContext);
+ assertEquals(aoStatus.size(), 1);
+ assertEquals(aoStatus.get(0).getId(), aoSubscription.getId());
+ assertEquals(aoStatus.get(0).getProductName(), aoProduct);
+ assertEquals(aoStatus.get(0).getBillingPeriod(), aoTerm);
+ assertEquals(aoStatus.get(0).getPhaseType(), aoSubscription.getCurrentPhase().getPhaseType());
+ assertEquals(aoStatus.get(0).getPriceList(), aoSubscription.getCurrentPriceList().getName());
+ assertEquals(aoStatus.get(0).getReason(), DryRunChangeReason.AO_NOT_AVAILABLE_IN_NEW_PLAN);
+
+ baseSubscription.changePlan(newBaseProduct, newBaseTerm, newBasePriceList, callContext);
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
+ assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+
+ // MOVE AFTER CHANGE
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS CANCELLED
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ assertEquals(aoSubscription.getState(), EntitlementState.CANCELLED);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testAddonCreateWithBundleAlign() throws CatalogApiException, SubscriptionBaseApiException {
+ final String aoProduct = "Telescopic-Scope";
+ final BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // This is just to double check our test catalog gives us what we want before we start the test
+ final PlanSpecifier planSpecifier = new PlanSpecifier(aoProduct,
+ ProductCategory.ADD_ON,
+ aoTerm,
+ aoPriceList);
+ final PlanAlignmentCreate alignement = catalog.planCreateAlignment(planSpecifier, clock.getUTCNow());
+ assertEquals(alignement, PlanAlignmentCreate.START_OF_BUNDLE);
+
+ testAddonCreateInternal(aoProduct, aoTerm, aoPriceList, alignement);
+ }
+
+ @Test(groups = "slow")
+ public void testAddonCreateWithSubscriptionAlign() throws SubscriptionBaseApiException, CatalogApiException {
+ final String aoProduct = "Laser-Scope";
+ final BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ final String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // This is just to double check our test catalog gives us what we want before we start the test
+ final PlanSpecifier planSpecifier = new PlanSpecifier(aoProduct,
+ ProductCategory.ADD_ON,
+ aoTerm,
+ aoPriceList);
+ final PlanAlignmentCreate alignement = catalog.planCreateAlignment(planSpecifier, clock.getUTCNow());
+ assertEquals(alignement, PlanAlignmentCreate.START_OF_SUBSCRIPTION);
+
+ testAddonCreateInternal(aoProduct, aoTerm, aoPriceList, alignement);
+ }
+
+ private void testAddonCreateInternal(final String aoProduct, final BillingPeriod aoTerm, final String aoPriceList, final PlanAlignmentCreate expAlignement) throws SubscriptionBaseApiException {
+ final String baseProduct = "Shotgun";
+ final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ final DefaultSubscriptionBase baseSubscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList);
+
+ // MOVE CLOCK 14 DAYS LATER
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(14));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ // CREATE ADDON
+ final DateTime beforeAOCreation = clock.getUTCNow();
+ DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList);
+ final DateTime afterAOCreation = clock.getUTCNow();
+
+ // CHECK EVERYTHING
+ Plan aoCurrentPlan = aoSubscription.getCurrentPlan();
+ assertNotNull(aoCurrentPlan);
+ assertEquals(aoCurrentPlan.getProduct().getName(), aoProduct);
+ assertEquals(aoCurrentPlan.getProduct().getCategory(), ProductCategory.ADD_ON);
+ assertEquals(aoCurrentPlan.getBillingPeriod(), aoTerm);
+
+ PlanPhase aoCurrentPhase = aoSubscription.getCurrentPhase();
+ assertNotNull(aoCurrentPhase);
+ assertEquals(aoCurrentPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ testUtil.assertDateWithin(aoSubscription.getStartDate(), beforeAOCreation, afterAOCreation);
+ assertEquals(aoSubscription.getBundleStartDate(), baseSubscription.getBundleStartDate());
+
+ // CHECK next AO PHASE EVENT IS INDEED A MONTH AFTER BP STARTED => BUNDLE ALIGNMENT
+ SubscriptionBaseTransition aoPendingTranstion = aoSubscription.getPendingTransition();
+ if (expAlignement == PlanAlignmentCreate.START_OF_BUNDLE) {
+ assertEquals(aoPendingTranstion.getEffectiveTransitionTime(), baseSubscription.getStartDate().plusMonths(1));
+ } else {
+ assertEquals(aoPendingTranstion.getEffectiveTransitionTime(), aoSubscription.getStartDate().plusMonths(1));
+ }
+
+ // ADD TWO PHASE EVENTS (BP + AO)
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ // MOVE THROUGH TIME TO GO INTO EVERGREEN
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(33));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // CHECK EVERYTHING AGAIN
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+
+ aoCurrentPlan = aoSubscription.getCurrentPlan();
+ assertNotNull(aoCurrentPlan);
+ assertEquals(aoCurrentPlan.getProduct().getName(), aoProduct);
+ assertEquals(aoCurrentPlan.getProduct().getCategory(), ProductCategory.ADD_ON);
+ assertEquals(aoCurrentPlan.getBillingPeriod(), aoTerm);
+
+ aoCurrentPhase = aoSubscription.getCurrentPhase();
+ assertNotNull(aoCurrentPhase);
+ assertEquals(aoCurrentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
+ aoPendingTranstion = aoSubscription.getPendingTransition();
+ assertNull(aoPendingTranstion);
+
+ assertListenerStatus();
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
new file mode 100644
index 0000000..1053238
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCancel.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.testng.Assert;
+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.Duration;
+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.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
+import org.killbill.billing.subscription.api.SubscriptionBillingApiException;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestUserApiCancel extends SubscriptionTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testCancelSubscriptionIMM() throws SubscriptionBaseApiException {
+ final DateTime init = clock.getUTCNow();
+
+ final String prod = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String planSet = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE
+ final DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, prod, term, planSet);
+ PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // ADVANCE TIME still in trial
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final DateTime future = clock.getUTCNow();
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+
+ assertEquals(subscription.getLastActiveProduct().getName(), prod);
+ assertEquals(subscription.getLastActivePriceList().getName(), planSet);
+ assertEquals(subscription.getLastActiveBillingPeriod(), term);
+ assertEquals(subscription.getLastActiveCategory(), ProductCategory.BASE);
+
+ // CANCEL in trial period to get IMM policy
+ subscription.cancel(callContext);
+ currentPhase = subscription.getCurrentPhase();
+ assertListenerStatus();
+
+ assertEquals(subscription.getLastActiveProduct().getName(), prod);
+ assertEquals(subscription.getLastActivePriceList().getName(), planSet);
+ assertEquals(subscription.getLastActiveBillingPeriod(), term);
+ assertEquals(subscription.getLastActiveCategory(), ProductCategory.BASE);
+
+ assertNull(currentPhase);
+ testUtil.checkNextPhaseChange(subscription, 0, null);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testCancelSubscriptionEOTWithChargeThroughDate() throws SubscriptionBillingApiException, SubscriptionBaseApiException {
+ final String prod = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String planSet = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE
+ DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, prod, term, planSet);
+ PlanPhase trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // NEXT PHASE
+ final DateTime expectedPhaseTrialChange = TestSubscriptionHelper.addDuration(subscription.getStartDate(), trialPhase.getDuration());
+ testUtil.checkNextPhaseChange(subscription, 1, expectedPhaseTrialChange);
+
+ // MOVE TO NEXT PHASE
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ assertListenerStatus();
+ trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ // SET CTD + RE READ SUBSCRIPTION + CHANGE PLAN
+ final Duration ctd = testUtil.getDurationMonth(1);
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(expectedPhaseTrialChange, ctd);
+ subscriptionInternalApi.setChargedThroughDate(subscription.getId(), newChargedThroughDate, internalCallContext);
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+
+ assertEquals(subscription.getLastActiveProduct().getName(), prod);
+ assertEquals(subscription.getLastActivePriceList().getName(), planSet);
+ assertEquals(subscription.getLastActiveBillingPeriod(), term);
+ assertEquals(subscription.getLastActiveCategory(), ProductCategory.BASE);
+
+ // CANCEL
+ subscription.cancel(callContext);
+ assertListenerStatus();
+
+ assertEquals(subscription.getLastActiveProduct().getName(), prod);
+ assertEquals(subscription.getLastActivePriceList().getName(), planSet);
+ assertEquals(subscription.getLastActiveBillingPeriod(), term);
+ assertEquals(subscription.getLastActiveCategory(), ProductCategory.BASE);
+
+ final DateTime futureEndDate = subscription.getFutureEndDate();
+ Assert.assertNotNull(futureEndDate);
+
+ // MOVE TO EOT + RECHECK
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ final DateTime future = clock.getUTCNow();
+ assertListenerStatus();
+
+ assertTrue(futureEndDate.compareTo(subscription.getEndDate()) == 0);
+
+ final PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNull(currentPhase);
+ testUtil.checkNextPhaseChange(subscription, 0, null);
+
+ assertEquals(subscription.getLastActiveProduct().getName(), prod);
+ assertEquals(subscription.getLastActivePriceList().getName(), planSet);
+ assertEquals(subscription.getLastActiveBillingPeriod(), term);
+ assertEquals(subscription.getLastActiveCategory(), ProductCategory.BASE);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testCancelSubscriptionEOTWithNoChargeThroughDate() throws SubscriptionBaseApiException {
+ final String prod = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String planSet = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE
+ final DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, prod, term, planSet);
+ PlanPhase trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // NEXT PHASE
+ final DateTime expectedPhaseTrialChange = TestSubscriptionHelper.addDuration(subscription.getStartDate(), trialPhase.getDuration());
+ testUtil.checkNextPhaseChange(subscription, 1, expectedPhaseTrialChange);
+
+ // MOVE TO NEXT PHASE
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+ trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+
+ // CANCEL
+ subscription.cancel(callContext);
+ assertListenerStatus();
+
+ final PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNull(currentPhase);
+ testUtil.checkNextPhaseChange(subscription, 0, null);
+
+ assertListenerStatus();
+ }
+
+ // Similar test to testCancelSubscriptionEOTWithChargeThroughDate except we uncancel and check things
+ // are as they used to be and we can move forward without hitting cancellation
+ @Test(groups = "slow")
+ public void testUncancel() throws SubscriptionBillingApiException, SubscriptionBaseApiException {
+ final String prod = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String planSet = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE
+ DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, prod, term, planSet);
+ final PlanPhase trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // NEXT PHASE
+ final DateTime expectedPhaseTrialChange = TestSubscriptionHelper.addDuration(subscription.getStartDate(), trialPhase.getDuration());
+ testUtil.checkNextPhaseChange(subscription, 1, expectedPhaseTrialChange);
+
+ // MOVE TO NEXT PHASE
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+ PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ // SET CTD + RE READ SUBSCRIPTION + CHANGE PLAN
+ final Duration ctd = testUtil.getDurationMonth(1);
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(expectedPhaseTrialChange, ctd);
+ subscriptionInternalApi.setChargedThroughDate(subscription.getId(), newChargedThroughDate, internalCallContext);
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+
+ // CANCEL EOT
+ subscription.cancel(callContext);
+
+ subscription.uncancel(callContext);
+
+ // MOVE TO EOT + RECHECK
+ testListener.pushExpectedEvent(NextEvent.UNCANCEL);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ final Plan currentPlan = subscription.getCurrentPlan();
+ assertEquals(currentPlan.getProduct().getName(), prod);
+ currentPhase = subscription.getCurrentPhase();
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ assertListenerStatus();
+ }
+}
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
new file mode 100644
index 0000000..34c9126
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.testng.Assert;
+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.Duration;
+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.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
+import org.killbill.billing.subscription.api.SubscriptionBillingApiException;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.user.ApiEvent;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
+
+ private void checkChangePlan(final DefaultSubscriptionBase subscription, final String expProduct, final ProductCategory expCategory,
+ final BillingPeriod expBillingPeriod, final PhaseType expPhase) {
+
+ final Plan currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), expProduct);
+ assertEquals(currentPlan.getProduct().getCategory(), expCategory);
+ assertEquals(currentPlan.getBillingPeriod(), expBillingPeriod);
+
+ final PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), expPhase);
+ }
+
+ @Test(groups = "slow")
+ public void testChangePlanBundleAlignEOTWithNoChargeThroughDate() {
+ tChangePlanBundleAlignEOTWithNoChargeThroughDate("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, "Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+ }
+
+ private void tChangePlanBundleAlignEOTWithNoChargeThroughDate(final String fromProd, final BillingPeriod fromTerm, final String fromPlanSet,
+ final String toProd, final BillingPeriod toTerm, final String toPlanSet) {
+ try {
+ // CREATE
+ final DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, fromProd, fromTerm, fromPlanSet);
+
+ // MOVE TO NEXT PHASE
+ PlanPhase currentPhase = subscription.getCurrentPhase();
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final DateTime futureNow = clock.getUTCNow();
+ final DateTime nextExpectedPhaseChange = TestSubscriptionHelper.addDuration(subscription.getStartDate(), currentPhase.getDuration());
+ assertTrue(futureNow.isAfter(nextExpectedPhaseChange));
+ assertListenerStatus();
+
+ // CHANGE PLAN
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ subscription.changePlan(toProd, toTerm, toPlanSet, callContext);
+ assertListenerStatus();
+
+ // CHECK CHANGE PLAN
+ currentPhase = subscription.getCurrentPhase();
+ checkChangePlan(subscription, toProd, ProductCategory.BASE, toTerm, PhaseType.EVERGREEN);
+
+ assertListenerStatus();
+ } catch (SubscriptionBaseApiException e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testChangePlanBundleAlignEOTWithChargeThroughDate() throws SubscriptionBillingApiException, SubscriptionBaseApiException {
+ testChangePlanBundleAlignEOTWithChargeThroughDate("Shotgun", BillingPeriod.ANNUAL, "gunclubDiscount", "Pistol", BillingPeriod.ANNUAL, "gunclubDiscount");
+ }
+
+ private void testChangePlanBundleAlignEOTWithChargeThroughDate(final String fromProd, final BillingPeriod fromTerm, final String fromPlanSet,
+ final String toProd, final BillingPeriod toTerm, final String toPlanSet) throws SubscriptionBillingApiException, SubscriptionBaseApiException {
+ // CREATE
+ DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, fromProd, fromTerm, fromPlanSet);
+ final PlanPhase trialPhase = subscription.getCurrentPhase();
+ final DateTime expectedPhaseTrialChange = TestSubscriptionHelper.addDuration(subscription.getStartDate(), trialPhase.getDuration());
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // MOVE TO NEXT PHASE
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+ PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertEquals(currentPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ // SET CTD
+ final Duration ctd = testUtil.getDurationMonth(1);
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(expectedPhaseTrialChange, ctd);
+ subscriptionInternalApi.setChargedThroughDate(subscription.getId(), newChargedThroughDate, internalCallContext);
+
+ // RE READ SUBSCRIPTION + CHANGE PLAN
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+ subscription.changePlan(toProd, toTerm, toPlanSet, callContext);
+ assertListenerStatus();
+
+ // CHECK CHANGE PLAN
+ currentPhase = subscription.getCurrentPhase();
+ checkChangePlan(subscription, fromProd, ProductCategory.BASE, fromTerm, PhaseType.DISCOUNT);
+
+ // NEXT PHASE
+ final DateTime nextExpectedPhaseChange = TestSubscriptionHelper.addDuration(expectedPhaseTrialChange, currentPhase.getDuration());
+ testUtil.checkNextPhaseChange(subscription, 2, nextExpectedPhaseChange);
+
+ // ALSO VERIFY PENDING CHANGE EVENT
+ final List<SubscriptionBaseEvent> events = dao.getPendingEventsForSubscription(subscription.getId(), internalCallContext);
+ assertTrue(events.get(0) instanceof ApiEvent);
+
+ // MOVE TO EOT
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+ currentPhase = subscription.getCurrentPhase();
+ checkChangePlan(subscription, toProd, ProductCategory.BASE, toTerm, PhaseType.DISCOUNT);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testChangePlanBundleAlignIMM() throws SubscriptionBaseApiException {
+ tChangePlanBundleAlignIMM("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, "Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+ }
+
+ private void tChangePlanBundleAlignIMM(final String fromProd, final BillingPeriod fromTerm, final String fromPlanSet,
+ final String toProd, final BillingPeriod toTerm, final String toPlanSet) throws SubscriptionBaseApiException {
+ final DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, fromProd, fromTerm, fromPlanSet);
+
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(3));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ // CHANGE PLAN IMM
+ subscription.changePlan(toProd, toTerm, toPlanSet, callContext);
+ checkChangePlan(subscription, toProd, ProductCategory.BASE, toTerm, PhaseType.TRIAL);
+
+ assertListenerStatus();
+
+ final PlanPhase currentPhase = subscription.getCurrentPhase();
+ final DateTime nextExpectedPhaseChange = TestSubscriptionHelper.addDuration(subscription.getStartDate(), currentPhase.getDuration());
+ testUtil.checkNextPhaseChange(subscription, 1, nextExpectedPhaseChange);
+
+ // NEXT PHASE
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(30));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ final DateTime futureNow = clock.getUTCNow();
+
+ assertTrue(futureNow.isAfter(nextExpectedPhaseChange));
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testChangePlanChangePlanAlignEOTWithChargeThroughDate() throws SubscriptionBillingApiException, SubscriptionBaseApiException {
+ tChangePlanChangePlanAlignEOTWithChargeThroughDate("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, "Assault-Rifle", BillingPeriod.ANNUAL, "rescue");
+ }
+
+ private void tChangePlanChangePlanAlignEOTWithChargeThroughDate(final String fromProd, final BillingPeriod fromTerm, final String fromPlanSet,
+ final String toProd, final BillingPeriod toTerm, final String toPlanSet) throws SubscriptionBillingApiException, SubscriptionBaseApiException {
+ DateTime currentTime = clock.getUTCNow();
+
+ DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, fromProd, fromTerm, fromPlanSet);
+ final PlanPhase trialPhase = subscription.getCurrentPhase();
+ final DateTime expectedPhaseTrialChange = TestSubscriptionHelper.addDuration(subscription.getStartDate(), trialPhase.getDuration());
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // MOVE TO NEXT PHASE
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ currentTime = clock.getUTCNow();
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ currentTime = clock.getUTCNow();
+ assertListenerStatus();
+
+ // SET CTD
+ final Duration ctd = testUtil.getDurationMonth(1);
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(expectedPhaseTrialChange, ctd);
+ subscriptionInternalApi.setChargedThroughDate(subscription.getId(), newChargedThroughDate, internalCallContext);
+
+ // RE READ SUBSCRIPTION + CHECK CURRENT PHASE
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+ PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ // CHANGE PLAN
+ currentTime = clock.getUTCNow();
+ subscription.changePlan(toProd, toTerm, toPlanSet, callContext);
+
+ checkChangePlan(subscription, fromProd, ProductCategory.BASE, fromTerm, PhaseType.EVERGREEN);
+
+ // CHECK CHANGE DID NOT KICK IN YET
+ assertListenerStatus();
+
+ // MOVE TO AFTER CTD
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ currentTime = clock.getUTCNow();
+ assertListenerStatus();
+
+ // CHECK CORRECT PRODUCT, PHASE, PLAN SET
+ final String currentProduct = subscription.getCurrentPlan().getProduct().getName();
+ assertNotNull(currentProduct);
+ assertEquals(currentProduct, toProd);
+ currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ // MOVE TIME ABOUT ONE MONTH BEFORE NEXT EXPECTED PHASE CHANGE
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(11));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ currentTime = clock.getUTCNow();
+ assertListenerStatus();
+
+ final DateTime nextExpectedPhaseChange = TestSubscriptionHelper.addDuration(newChargedThroughDate, currentPhase.getDuration());
+ testUtil.checkNextPhaseChange(subscription, 1, nextExpectedPhaseChange);
+
+ // MOVE TIME RIGHT AFTER NEXT EXPECTED PHASE CHANGE
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ currentTime = clock.getUTCNow();
+ assertListenerStatus();
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testMultipleChangeLastIMM() throws SubscriptionBillingApiException, SubscriptionBaseApiException {
+ DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, "Assault-Rifle", BillingPeriod.MONTHLY, "gunclubDiscount");
+ final PlanPhase trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // MOVE TO NEXT PHASE
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ assertListenerStatus();
+
+ // SET CTD
+ final List<Duration> durationList = new ArrayList<Duration>();
+ durationList.add(trialPhase.getDuration());
+ //durationList.add(subscription.getCurrentPhase().getDuration());
+ final DateTime startDiscountPhase = TestSubscriptionHelper.addDuration(subscription.getStartDate(), durationList);
+ final Duration ctd = testUtil.getDurationMonth(1);
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(startDiscountPhase, ctd);
+ subscriptionInternalApi.setChargedThroughDate(subscription.getId(), newChargedThroughDate, internalCallContext);
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+
+ // CHANGE EOT
+ subscription.changePlan("Pistol", BillingPeriod.MONTHLY, "gunclubDiscount", callContext);
+ assertListenerStatus();
+
+ // CHANGE
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ subscription.changePlan("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount", callContext);
+ assertListenerStatus();
+
+ final Plan currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), "Assault-Rifle");
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.ANNUAL);
+
+ final PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testMultipleChangeLastEOT() throws SubscriptionBillingApiException, SubscriptionBaseApiException {
+ DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, "Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount");
+ final PlanPhase trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // SET CTD
+ final List<Duration> durationList = new ArrayList<Duration>();
+ durationList.add(trialPhase.getDuration());
+ final DateTime startDiscountPhase = TestSubscriptionHelper.addDuration(subscription.getStartDate(), durationList);
+ final Duration ctd = testUtil.getDurationMonth(1);
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(startDiscountPhase, ctd);
+ subscriptionInternalApi.setChargedThroughDate(subscription.getId(), newChargedThroughDate, internalCallContext);
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+
+ // CHANGE EOT
+ subscription.changePlan("Shotgun", BillingPeriod.MONTHLY, "gunclubDiscount", callContext);
+ assertListenerStatus();
+
+ // CHANGE EOT
+ subscription.changePlan("Pistol", BillingPeriod.ANNUAL, "gunclubDiscount", callContext);
+ assertListenerStatus();
+
+ // CHECK NO CHANGE OCCURED YET
+ Plan currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), "Assault-Rifle");
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.ANNUAL);
+
+ PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ // ACTIVATE CHANGE BY MOVING AFTER CTD
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), "Pistol");
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.ANNUAL);
+
+ currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ // MOVE TO NEXT PHASE
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusMonths(6));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+
+ currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), "Pistol");
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.ANNUAL);
+
+ currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testCorrectPhaseAlignmentOnChange() throws SubscriptionBaseApiException {
+ DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+ PlanPhase trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // MOVE 2 DAYS AHEAD
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(2));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ // CHANGE IMMEDIATE TO A 3 PHASES PLAN
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ subscription.changePlan("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount", callContext);
+ assertListenerStatus();
+
+ // CHECK EVERYTHING LOOKS CORRECT
+ final Plan currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), "Assault-Rifle");
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.ANNUAL);
+
+ trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // MOVE AFTER TRIAL PERIOD -> DISCOUNT
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(30));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ assertListenerStatus();
+
+ trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+
+ final DateTime expectedNextPhaseDate = subscription.getStartDate().plusDays(30).plusMonths(6);
+ final SubscriptionBaseTransition nextPhase = subscription.getPendingTransition();
+
+ final DateTime nextPhaseEffectiveDate = nextPhase.getEffectiveTransitionTime();
+ assertEquals(nextPhaseEffectiveDate, expectedNextPhaseDate);
+
+ assertListenerStatus();
+ }
+}
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
new file mode 100644
index 0000000..22eeaba
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiCreate.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.subscription.DefaultSubscriptionTestInitializer;
+import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.phase.PhaseEvent;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestUserApiCreate extends SubscriptionTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testCreateBundlesWithSameExternalKeys() throws SubscriptionBaseApiException {
+ final DateTime init = clock.getUTCNow();
+ final DateTime requestedDate = init.minusYears(1);
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.PHASE);
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.createSubscription(bundle.getId(),
+ testUtil.getProductSpecifier(productName, planSetName, term, null), requestedDate, internalCallContext);
+ assertListenerStatus();
+ assertNotNull(subscription);
+
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ subscription.cancelWithDate(clock.getUTCNow(), callContext);
+ assertListenerStatus();
+
+ final SubscriptionBaseBundle newBundle = subscriptionInternalApi.createBundleForAccount(bundle.getAccountId(), DefaultSubscriptionTestInitializer.DEFAULT_BUNDLE_KEY, internalCallContext);
+ 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), requestedDate, internalCallContext);
+
+ subscriptionInternalApi.updateExternalKey(newBundle.getId(), "myNewSuperKey", internalCallContext);
+
+ final SubscriptionBaseBundle bundleWithNewKey = subscriptionInternalApi.getBundleFromId(newBundle.getId(), internalCallContext);
+ assertEquals(bundleWithNewKey.getExternalKey(), "myNewSuperKey");
+
+ assertListenerStatus();
+ assertNotNull(newSubscription);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateWithRequestedDate() throws SubscriptionBaseApiException {
+ final DateTime init = clock.getUTCNow();
+ final DateTime requestedDate = init.minusYears(1);
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.CREATE);
+
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.createSubscription(bundle.getId(),
+ testUtil.getProductSpecifier(productName, planSetName, term, null), requestedDate, internalCallContext);
+ assertNotNull(subscription);
+
+ //
+ // In addition to Alignment phase we also test SubscriptionBaseTransition eventIds and created dates.
+ // Keep tracks of row events to compare with ids and created dates returned by SubscriptionBaseTransition later.
+ //
+ final List<SubscriptionBaseEvent> events = subscription.getEvents();
+ Assert.assertEquals(events.size(), 2);
+
+ final SubscriptionBaseEvent trialEvent = events.get(0);
+ final SubscriptionBaseEvent phaseEvent = events.get(1);
+
+ assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+ //assertEquals(subscription.getAccount(), account.getId());
+ assertEquals(subscription.getBundleId(), bundle.getId());
+ assertEquals(subscription.getStartDate(), requestedDate);
+
+ assertListenerStatus();
+
+ final SubscriptionBaseTransition transition = subscription.getPreviousTransition();
+
+ assertEquals(transition.getPreviousEventId(), trialEvent.getId());
+ assertEquals(transition.getNextEventId(), phaseEvent.getId());
+
+ assertEquals(transition.getPreviousEventCreatedDate().compareTo(trialEvent.getCreatedDate()), 0);
+ assertEquals(transition.getNextEventCreatedDate().compareTo(phaseEvent.getCreatedDate()), 0);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateWithInitialPhase() throws SubscriptionBaseApiException {
+ final DateTime init = clock.getUTCNow();
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ testListener.pushExpectedEvent(NextEvent.CREATE);
+
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.createSubscription(bundle.getId(),
+ testUtil.getProductSpecifier(productName, planSetName, term, PhaseType.EVERGREEN), clock.getUTCNow(), internalCallContext);
+ assertNotNull(subscription);
+
+ assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+ //assertEquals(subscription.getAccount(), account.getId());
+ assertEquals(subscription.getBundleId(), bundle.getId());
+ testUtil.assertDateWithin(subscription.getStartDate(), init, clock.getUTCNow());
+ testUtil.assertDateWithin(subscription.getBundleStartDate(), init, clock.getUTCNow());
+
+ final Plan currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), productName);
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ final PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testSimpleCreateSubscription() throws SubscriptionBaseApiException {
+ final DateTime init = clock.getUTCNow();
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+ final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ testListener.pushExpectedEvent(NextEvent.CREATE);
+
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.createSubscription(bundle.getId(),
+ testUtil.getProductSpecifier(productName, planSetName, term, null),
+ clock.getUTCNow(), internalCallContext);
+ assertNotNull(subscription);
+
+ assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+ //assertEquals(subscription.getAccount(), account.getId());
+ assertEquals(subscription.getBundleId(), bundle.getId());
+ testUtil.assertDateWithin(subscription.getStartDate(), init, clock.getUTCNow());
+ testUtil.assertDateWithin(subscription.getBundleStartDate(), init, clock.getUTCNow());
+
+ final Plan currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), productName);
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.MONTHLY);
+
+ final PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL);
+ assertListenerStatus();
+
+ final List<SubscriptionBaseEvent> events = dao.getPendingEventsForSubscription(subscription.getId(), internalCallContext);
+ assertNotNull(events);
+ testUtil.printEvents(events);
+ assertTrue(events.size() == 1);
+ assertTrue(events.get(0) instanceof PhaseEvent);
+ final DateTime nextPhaseChange = ((PhaseEvent) events.get(0)).getEffectiveDate();
+ final DateTime nextExpectedPhaseChange = TestSubscriptionHelper.addDuration(subscription.getStartDate(), currentPhase.getDuration());
+ assertEquals(nextPhaseChange, nextExpectedPhaseChange);
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+
+ final DateTime futureNow = clock.getUTCNow();
+ assertTrue(futureNow.isAfter(nextPhaseChange));
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testSimpleSubscriptionThroughPhases() throws SubscriptionBaseApiException {
+ final String productName = "Pistol";
+ final BillingPeriod term = BillingPeriod.ANNUAL;
+ final String planSetName = "gunclubDiscount";
+
+ testListener.pushExpectedEvent(NextEvent.CREATE);
+
+ // CREATE SUBSCRIPTION
+ DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.createSubscription(bundle.getId(),
+ testUtil.getProductSpecifier(productName, planSetName, term, null), clock.getUTCNow(), internalCallContext);
+ assertNotNull(subscription);
+
+ PlanPhase currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.TRIAL);
+ assertListenerStatus();
+
+ // MOVE TO DISCOUNT PHASE
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+ currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ // MOVE TO EVERGREEN PHASE + RE-READ SUBSCRIPTION
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusYears(1));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+ currentPhase = subscription.getCurrentPhase();
+ assertNotNull(currentPhase);
+ assertEquals(currentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testSubscriptionWithAddOn() throws SubscriptionBaseApiException {
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.ANNUAL;
+ final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ testListener.pushExpectedEvent(NextEvent.CREATE);
+
+ final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.createSubscription(bundle.getId(),
+ testUtil.getProductSpecifier(productName, planSetName, term, null), clock.getUTCNow(), internalCallContext);
+ assertNotNull(subscription);
+
+ assertListenerStatus();
+ }
+}
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
new file mode 100644
index 0000000..ed778b4
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+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.Duration;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.clock.DefaultClock;
+import org.killbill.billing.subscription.SubscriptionTestSuiteNoDB;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class TestUserApiError extends SubscriptionTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testCreateSubscriptionBadCatalog() {
+ // WRONG PRODUCTS
+ tCreateSubscriptionInternal(bundle.getId(), null, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.CAT_NULL_PRODUCT_NAME);
+ tCreateSubscriptionInternal(bundle.getId(), "Whatever", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.CAT_NO_SUCH_PRODUCT);
+
+ // TODO: MARTIN TO FIX WITH CORRECT ERROR CODE. RIGHT NOW NPE
+
+ // WRONG BILLING PERIOD
+ tCreateSubscriptionInternal(bundle.getId(), "Shotgun", null, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.CAT_PLAN_NOT_FOUND);
+ // WRONG PLAN SET
+ tCreateSubscriptionInternal(bundle.getId(), "Shotgun", BillingPeriod.ANNUAL, "Whatever", ErrorCode.CAT_PRICE_LIST_NOT_FOUND);
+ }
+
+ @Test(groups = "fast")
+ public void testCreateSubscriptionNoBundle() {
+ tCreateSubscriptionInternal(null, "Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.SUB_CREATE_NO_BUNDLE);
+ }
+
+ @Test(groups = "fast")
+ public void testCreateSubscriptionNoBP() {
+ tCreateSubscriptionInternal(bundle.getId(), "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.SUB_CREATE_NO_BP);
+ }
+
+ @Test(groups = "fast")
+ public void testCreateSubscriptionBPExists() throws SubscriptionBaseApiException {
+ testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME);
+ tCreateSubscriptionInternal(bundle.getId(), "Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.SUB_CREATE_BP_EXISTS);
+ }
+
+ @Test(groups = "fast")
+ public void testCreateSubscriptionAddOnNotAvailable() throws SubscriptionBaseApiException {
+ final UUID accountId = UUID.randomUUID();
+ final SubscriptionBaseBundle aoBundle = subscriptionInternalApi.createBundleForAccount(accountId, "myAOBundle", internalCallContext);
+ testUtil.createSubscriptionWithBundle(aoBundle.getId(), "Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ tCreateSubscriptionInternal(aoBundle.getId(), "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.SUB_CREATE_AO_NOT_AVAILABLE);
+ }
+
+ @Test(groups = "fast")
+ public void testCreateSubscriptionAddOnIncluded() throws SubscriptionBaseApiException {
+ final UUID accountId = UUID.randomUUID();
+ final SubscriptionBaseBundle aoBundle = subscriptionInternalApi.createBundleForAccount(accountId, "myAOBundle", internalCallContext);
+ testUtil.createSubscriptionWithBundle(aoBundle.getId(), "Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ tCreateSubscriptionInternal(aoBundle.getId(), "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.SUB_CREATE_AO_ALREADY_INCLUDED);
+ }
+
+ private void tCreateSubscriptionInternal(@Nullable final UUID bundleId, @Nullable final String productName,
+ @Nullable final BillingPeriod term, final String planSet, final ErrorCode expected) {
+ try {
+ subscriptionInternalApi.createSubscription(bundleId,
+ testUtil.getProductSpecifier(productName, planSet, term, null),
+ clock.getUTCNow(), internalCallContext);
+ Assert.fail("Exception expected, error code: " + expected);
+ } catch (SubscriptionBaseApiException e) {
+ assertEquals(e.getCode(), expected.getCode());
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testChangeSubscriptionNonActive() throws SubscriptionBaseApiException {
+ final SubscriptionBase subscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ subscription.cancelWithDate(clock.getUTCNow(), callContext);
+ try {
+ subscription.changePlanWithDate("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, clock.getUTCNow(), callContext);
+ } catch (SubscriptionBaseApiException e) {
+ assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_NON_ACTIVE.getCode());
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testChangeSubscriptionWithPolicy() throws Exception {
+ final SubscriptionBase subscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ try {
+ subscription.changePlanWithPolicy("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, BillingActionPolicy.ILLEGAL, callContext);
+ Assert.fail();
+ } catch (SubscriptionBaseError error) {
+ assertTrue(true);
+ assertEquals(subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+ }
+
+ // Assume the call takes less than a second
+ assertEquals(DefaultClock.truncateMs(subscription.changePlanWithPolicy("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, BillingActionPolicy.IMMEDIATE, callContext)),
+ DefaultClock.truncateMs(clock.getUTCNow()));
+ assertEquals(subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
+ }
+
+ @Test(groups = "fast")
+ public void testChangeSubscriptionFutureCancelled() throws SubscriptionBaseApiException {
+ SubscriptionBase subscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+ final PlanPhase trialPhase = subscription.getCurrentPhase();
+
+ // MOVE TO NEXT PHASE
+ final PlanPhase currentPhase = subscription.getCurrentPhase();
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(31));
+ clock.addDeltaFromReality(it.toDurationMillis());
+ assertListenerStatus();
+
+ // SET CTD TO CANCEL IN FUTURE
+ final DateTime expectedPhaseTrialChange = TestSubscriptionHelper.addDuration(subscription.getStartDate(), trialPhase.getDuration());
+ final Duration ctd = testUtil.getDurationMonth(1);
+ final DateTime newChargedThroughDate = TestSubscriptionHelper.addDuration(expectedPhaseTrialChange, ctd);
+ subscriptionInternalApi.setChargedThroughDate(subscription.getId(), newChargedThroughDate, internalCallContext);
+
+ subscription = subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+
+ subscription.cancelWithPolicy(BillingActionPolicy.END_OF_TERM, callContext);
+ try {
+ subscription.changePlanWithDate("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, clock.getUTCNow(), callContext);
+ } catch (SubscriptionBaseApiException e) {
+ assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_FUTURE_CANCELLED.getCode());
+ }
+
+ assertListenerStatus();
+ }
+
+ @Test(groups = "fast")
+ public void testUncancelBadState() throws SubscriptionBaseApiException {
+ final SubscriptionBase subscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+ try {
+ subscription.uncancel(callContext);
+ } catch (SubscriptionBaseApiException e) {
+ assertEquals(e.getCode(), ErrorCode.SUB_UNCANCEL_BAD_STATE.getCode());
+ }
+ assertListenerStatus();
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiRecreate.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiRecreate.java
new file mode 100644
index 0000000..b87adb7
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiRecreate.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.api.user;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+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.PriceListSet;
+import org.killbill.billing.subscription.SubscriptionTestSuiteWithEmbeddedDB;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+public abstract class TestUserApiRecreate extends SubscriptionTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testRecreateWithBPCanceledThroughSubscription() throws SubscriptionBaseApiException {
+ testCreateAndRecreate(false);
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testCreateWithBPCanceledFromUserApi() throws SubscriptionBaseApiException {
+ testCreateAndRecreate(true);
+ assertListenerStatus();
+ }
+
+ private DefaultSubscriptionBase testCreateAndRecreate(final boolean fromUserAPi) throws SubscriptionBaseApiException {
+ final DateTime init = clock.getUTCNow();
+ final DateTime requestedDate = init.minusYears(1);
+
+ String productName = "Shotgun";
+ BillingPeriod term = BillingPeriod.MONTHLY;
+ String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.CREATE);
+ DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) subscriptionInternalApi.createSubscription(bundle.getId(),
+ testUtil.getProductSpecifier(productName, planSetName, term, null), requestedDate, internalCallContext);
+ assertNotNull(subscription);
+ assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+ assertEquals(subscription.getBundleId(), bundle.getId());
+ assertEquals(subscription.getStartDate(), requestedDate);
+ assertEquals(productName, subscription.getCurrentPlan().getProduct().getName());
+
+ assertListenerStatus();
+
+ // CREATE (AGAIN) WITH NEW PRODUCT
+ productName = "Pistol";
+ term = BillingPeriod.MONTHLY;
+ planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+ try {
+ if (fromUserAPi) {
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.createSubscription(bundle.getId(),
+ testUtil.getProductSpecifier(productName, planSetName, term, null), requestedDate, internalCallContext);
+ } else {
+ subscription.recreate(testUtil.getProductSpecifier(productName, planSetName, term, null), requestedDate, callContext);
+ }
+ Assert.fail("Expected Create API to fail since BP already exists");
+ } catch (SubscriptionBaseApiException e) {
+ assertTrue(true);
+ }
+
+ // NOW CANCEL ADN THIS SHOULD WORK
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ subscription.cancelWithDate(null, callContext);
+
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.RE_CREATE);
+
+ // Avoid ordering issue for events at exact same date; this is actually a real good test,
+ // we test it at Beatrix level. At this level that would work for sql tests but not for in memory.
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException ignored) {
+ }
+
+ if (fromUserAPi) {
+ subscription = (DefaultSubscriptionBase) subscriptionInternalApi.createSubscription(bundle.getId(),
+ testUtil.getProductSpecifier(productName, planSetName, term, null), requestedDate, internalCallContext);
+ } else {
+ subscription.recreate(testUtil.getProductSpecifier(productName, planSetName, term, null), clock.getUTCNow(), callContext);
+ }
+ assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
+ assertEquals(subscription.getBundleId(), bundle.getId());
+ assertEquals(subscription.getStartDate(), requestedDate);
+ assertEquals(productName, subscription.getCurrentPlan().getProduct().getName());
+
+ return subscription;
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/DefaultSubscriptionTestInitializer.java b/subscription/src/test/java/org/killbill/billing/subscription/DefaultSubscriptionTestInitializer.java
new file mode 100644
index 0000000..28b7592
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/DefaultSubscriptionTestInitializer.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.DefaultCatalogService;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.mock.MockAccountBuilder;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseService;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import static org.testng.Assert.assertNotNull;
+
+public class DefaultSubscriptionTestInitializer implements SubscriptionTestInitializer {
+
+ public static final String DEFAULT_BUNDLE_KEY = "myDefaultBundle";
+
+ protected static final Logger log = LoggerFactory.getLogger(DefaultSubscriptionTestInitializer.class);
+
+ public DefaultSubscriptionTestInitializer() {
+
+ }
+
+ public Catalog initCatalog(final CatalogService catalogService) throws Exception {
+
+ ((DefaultCatalogService) catalogService).loadCatalog();
+ final Catalog catalog = catalogService.getFullCatalog();
+ assertNotNull(catalog);
+ return catalog;
+ }
+
+ public AccountData initAccountData() {
+ final AccountData accountData = new MockAccountBuilder().name(UUID.randomUUID().toString())
+ .firstNameLength(6)
+ .email(UUID.randomUUID().toString())
+ .phone(UUID.randomUUID().toString())
+ .migrated(false)
+ .isNotifiedForInvoices(false)
+ .externalKey(UUID.randomUUID().toString())
+ .billingCycleDayLocal(1)
+ .currency(Currency.USD)
+ .paymentMethodId(UUID.randomUUID())
+ .timeZone(DateTimeZone.forID("Europe/Paris"))
+ .build();
+
+ assertNotNull(accountData);
+ return accountData;
+ }
+
+ public SubscriptionBaseBundle initBundle(final SubscriptionBaseInternalApi subscriptionApi, final InternalCallContext callContext) throws Exception {
+ final UUID accountId = UUID.randomUUID();
+ final SubscriptionBaseBundle bundle = subscriptionApi.createBundleForAccount(accountId, DEFAULT_BUNDLE_KEY, callContext);
+ assertNotNull(bundle);
+ return bundle;
+ }
+
+ public void startTestFamework(final TestApiListener testListener,
+ final ClockMock clock,
+ final BusService busService,
+ final SubscriptionBaseService subscriptionBaseService) throws Exception {
+ log.debug("STARTING TEST FRAMEWORK");
+
+ resetTestListener(testListener);
+
+ resetClockToStartOfTest(clock);
+
+ startBusAndRegisterListener(busService, testListener);
+
+ restartSubscriptionService(subscriptionBaseService);
+
+ log.debug("STARTED TEST FRAMEWORK");
+ }
+
+ public void stopTestFramework(final TestApiListener testListener,
+ final BusService busService,
+ final SubscriptionBaseService subscriptionBaseService) throws Exception {
+ log.debug("STOPPING TEST FRAMEWORK");
+ stopBusAndUnregisterListener(busService, testListener);
+
+ stopSubscriptionService(subscriptionBaseService);
+
+ log.debug("STOPPED TEST FRAMEWORK");
+ }
+
+ private void resetTestListener(final TestApiListener testListener) {
+ // RESET LIST OF EXPECTED EVENTS
+ if (testListener != null) {
+ testListener.reset();
+ }
+ }
+
+ private void resetClockToStartOfTest(final ClockMock clock) {
+ clock.resetDeltaFromReality();
+
+ // Date at which all tests start-- we create the date object here after the system properties which set the JVM in UTC have been set.
+ final DateTime testStartDate = new DateTime(2012, 5, 7, 0, 3, 42, 0);
+ clock.setDeltaFromReality(testStartDate.getMillis() - clock.getUTCNow().getMillis());
+ }
+
+ private void startBusAndRegisterListener(final BusService busService, final TestApiListener testListener) throws Exception {
+ busService.getBus().start();
+ busService.getBus().register(testListener);
+ }
+
+ private void restartSubscriptionService(final SubscriptionBaseService subscriptionBaseService) {
+ // START NOTIFICATION QUEUE FOR SUBSCRIPTION
+ ((DefaultSubscriptionBaseService) subscriptionBaseService).initialize();
+ ((DefaultSubscriptionBaseService) subscriptionBaseService).start();
+ }
+
+ private void stopBusAndUnregisterListener(final BusService busService, final TestApiListener testListener) throws Exception {
+ busService.getBus().unregister(testListener);
+ busService.getBus().stop();
+ }
+
+ private void stopSubscriptionService(final SubscriptionBaseService subscriptionBaseService) throws Exception {
+ ((DefaultSubscriptionBaseService) subscriptionBaseService).stop();
+ }
+}
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
new file mode 100644
index 0000000..a52cd25
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.dao;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.clock.Clock;
+import org.killbill.billing.entitlement.api.SubscriptionApiException;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationQueue;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData.BundleMigrationData;
+import org.killbill.billing.subscription.api.migration.AccountMigrationData.SubscriptionMigrationData;
+import org.killbill.billing.subscription.api.timeline.SubscriptionDataRepair;
+import org.killbill.billing.subscription.api.transfer.TransferCancelData;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService;
+import org.killbill.billing.subscription.engine.core.SubscriptionNotificationKey;
+import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
+import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
+import org.killbill.billing.subscription.events.user.ApiEvent;
+import org.killbill.billing.subscription.events.user.ApiEventType;
+import org.killbill.billing.util.entity.DefaultPagination;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+import org.killbill.billing.util.entity.dao.MockEntityDaoBase;
+
+import com.google.inject.Inject;
+
+public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBundleModelDao, SubscriptionBaseBundle, SubscriptionApiException> implements SubscriptionDao {
+
+ protected static final Logger log = LoggerFactory.getLogger(SubscriptionDao.class);
+
+ private final List<SubscriptionBaseBundle> bundles;
+ private final List<SubscriptionBase> subscriptions;
+ private final TreeSet<SubscriptionBaseEvent> events;
+ private final Clock clock;
+ private final NotificationQueueService notificationQueueService;
+ private final CatalogService catalogService;
+
+ @Inject
+ public MockSubscriptionDaoMemory(final Clock clock,
+ final NotificationQueueService notificationQueueService,
+ final CatalogService catalogService) {
+ super();
+ this.clock = clock;
+ this.catalogService = catalogService;
+ this.notificationQueueService = notificationQueueService;
+ this.bundles = new ArrayList<SubscriptionBaseBundle>();
+ this.subscriptions = new ArrayList<SubscriptionBase>();
+ this.events = new TreeSet<SubscriptionBaseEvent>();
+ }
+
+ public void reset() {
+ bundles.clear();
+ subscriptions.clear();
+ events.clear();
+ }
+
+ @Override
+ public List<SubscriptionBaseBundle> getSubscriptionBundleForAccount(final UUID accountId, final InternalTenantContext context) {
+ final List<SubscriptionBaseBundle> results = new ArrayList<SubscriptionBaseBundle>();
+ for (final SubscriptionBaseBundle cur : bundles) {
+ if (cur.getAccountId().equals(accountId)) {
+ results.add(cur);
+ }
+ }
+ return results;
+ }
+
+ @Override
+ public List<SubscriptionBaseBundle> getSubscriptionBundlesForKey(final String bundleKey, final InternalTenantContext context) {
+ final List<SubscriptionBaseBundle> results = new ArrayList<SubscriptionBaseBundle>();
+ for (final SubscriptionBaseBundle cur : bundles) {
+ if (cur.getExternalKey().equals(bundleKey)) {
+ results.add(cur);
+ }
+ }
+ return results;
+ }
+
+ @Override
+ public Pagination<SubscriptionBundleModelDao> searchSubscriptionBundles(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+ final List<SubscriptionBundleModelDao> results = new LinkedList<SubscriptionBundleModelDao>();
+ for (final SubscriptionBundleModelDao bundleModelDao : getAll(context)) {
+ if (bundleModelDao.getId().toString().equals(searchKey) ||
+ bundleModelDao.getExternalKey().equals(searchKey) ||
+ bundleModelDao.getAccountId().toString().equals(searchKey)) {
+ results.add(bundleModelDao);
+ }
+ }
+
+ return DefaultPagination.<SubscriptionBundleModelDao>build(offset, limit, results);
+ }
+
+ @Override
+ public List<UUID> getNonAOSubscriptionIdsForKey(final String bundleKey, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public SubscriptionBaseBundle getSubscriptionBundleFromId(final UUID bundleId, final InternalTenantContext context) {
+ for (final SubscriptionBaseBundle cur : bundles) {
+ if (cur.getId().equals(bundleId)) {
+ return cur;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public List<SubscriptionBaseBundle> getSubscriptionBundlesForAccountAndKey(final UUID accountId, final String bundleKey, final InternalTenantContext context) {
+ final List<SubscriptionBaseBundle> results = new ArrayList<SubscriptionBaseBundle>();
+ for (final SubscriptionBaseBundle cur : bundles) {
+ if (cur.getExternalKey().equals(bundleKey) && cur.getAccountId().equals(accountId)) {
+ results.add(cur);
+ }
+ }
+ return results;
+ }
+
+ @Override
+ public SubscriptionBaseBundle createSubscriptionBundle(final DefaultSubscriptionBaseBundle bundle, final InternalCallContext context) {
+ bundles.add(bundle);
+ return getSubscriptionBundleFromId(bundle.getId(), context);
+ }
+
+ @Override
+ public SubscriptionBase getSubscriptionFromId(final UUID subscriptionId, final InternalTenantContext context) {
+ for (final SubscriptionBase cur : subscriptions) {
+ if (cur.getId().equals(subscriptionId)) {
+ return buildSubscription((DefaultSubscriptionBase) cur, context);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public UUID getAccountIdFromSubscriptionId(final UUID subscriptionId, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ @Override
+ public List<SubscriptionBase> getSubscriptionsForAccountAndKey(final UUID accountId, final String bundleKey, final InternalTenantContext callcontext) {
+
+ for (final SubscriptionBaseBundle cur : bundles) {
+ if (cur.getExternalKey().equals(bundleKey) && cur.getAccountId().equals(bundleKey)) {
+ return getSubscriptions(cur.getId(), callcontext);
+ }
+ }
+ return Collections.emptyList();
+ }
+ */
+
+ @Override
+ public void createSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> initialEvents,
+ final InternalCallContext context) {
+ synchronized (events) {
+ events.addAll(initialEvents);
+ for (final SubscriptionBaseEvent cur : initialEvents) {
+ recordFutureNotificationFromTransaction(null, cur.getEffectiveDate(), new SubscriptionNotificationKey(cur.getId()), context);
+ }
+ }
+ final SubscriptionBase updatedSubscription = buildSubscription(subscription, context);
+ subscriptions.add(updatedSubscription);
+ }
+
+ @Override
+ public void recreateSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> recreateEvents, final InternalCallContext context) {
+ synchronized (events) {
+ events.addAll(recreateEvents);
+ for (final SubscriptionBaseEvent cur : recreateEvents) {
+ recordFutureNotificationFromTransaction(null, cur.getEffectiveDate(), new SubscriptionNotificationKey(cur.getId()), context);
+ }
+ }
+ }
+
+ @Override
+ public List<SubscriptionBase> getSubscriptions(final UUID bundleId, final InternalTenantContext context) {
+ final List<SubscriptionBase> results = new ArrayList<SubscriptionBase>();
+ for (final SubscriptionBase cur : subscriptions) {
+ if (cur.getBundleId().equals(bundleId)) {
+ results.add(buildSubscription((DefaultSubscriptionBase) cur, context));
+ }
+ }
+ return results;
+ }
+
+ @Override
+ public Map<UUID, List<SubscriptionBase>> getSubscriptionsForAccount(final InternalTenantContext context) {
+ final Map<UUID, List<SubscriptionBase>> results = new HashMap<UUID, List<SubscriptionBase>>();
+ for (final SubscriptionBase cur : subscriptions) {
+ if (results.get(cur.getBundleId()) == null) {
+ results.put(cur.getBundleId(), new LinkedList<SubscriptionBase>());
+ }
+ results.get(cur.getBundleId()).add(buildSubscription((DefaultSubscriptionBase) cur, context));
+ }
+ return results;
+ }
+
+ @Override
+ public List<SubscriptionBaseEvent> getEventsForSubscription(final UUID subscriptionId, final InternalTenantContext context) {
+ synchronized (events) {
+ final List<SubscriptionBaseEvent> results = new LinkedList<SubscriptionBaseEvent>();
+ for (final SubscriptionBaseEvent cur : events) {
+ if (cur.getSubscriptionId().equals(subscriptionId)) {
+ results.add(cur);
+ }
+ }
+ return results;
+ }
+ }
+
+ @Override
+ public List<SubscriptionBaseEvent> getPendingEventsForSubscription(final UUID subscriptionId, final InternalTenantContext context) {
+ synchronized (events) {
+ final List<SubscriptionBaseEvent> results = new LinkedList<SubscriptionBaseEvent>();
+ for (final SubscriptionBaseEvent cur : events) {
+ if (cur.isActive() &&
+ cur.getEffectiveDate().isAfter(clock.getUTCNow()) &&
+ cur.getSubscriptionId().equals(subscriptionId)) {
+ results.add(cur);
+ }
+ }
+ return results;
+ }
+ }
+
+ @Override
+ public SubscriptionBase getBaseSubscription(final UUID bundleId, final InternalTenantContext context) {
+ for (final SubscriptionBase cur : subscriptions) {
+ if (cur.getBundleId().equals(bundleId) &&
+ cur.getCurrentPlan().getProduct().getCategory() == ProductCategory.BASE) {
+ return buildSubscription((DefaultSubscriptionBase) cur, context);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void createNextPhaseEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent nextPhase, final InternalCallContext context) {
+ cancelNextPhaseEvent(subscription.getId(), context);
+ insertEvent(nextPhase, context);
+ }
+
+ private SubscriptionBase buildSubscription(final DefaultSubscriptionBase in, final InternalTenantContext context) {
+ final DefaultSubscriptionBase subscription = new DefaultSubscriptionBase(new SubscriptionBuilder(in), null, clock);
+ if (events.size() > 0) {
+ subscription.rebuildTransitions(getEventsForSubscription(in.getId(), context), catalogService.getFullCatalog());
+ }
+ return subscription;
+
+ }
+
+ @Override
+ public void updateChargedThroughDate(final DefaultSubscriptionBase subscription, final InternalCallContext context) {
+ boolean found = false;
+ final Iterator<SubscriptionBase> it = subscriptions.iterator();
+ while (it.hasNext()) {
+ final SubscriptionBase cur = it.next();
+ if (cur.getId().equals(subscription.getId())) {
+ found = true;
+ it.remove();
+ break;
+ }
+ }
+ if (found) {
+ subscriptions.add(subscription);
+ }
+ }
+
+ @Override
+ public void cancelSubscription(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent cancelEvent,
+ final InternalCallContext context, final int seqId) {
+ synchronized (events) {
+ cancelNextPhaseEvent(subscription.getId(), context);
+ insertEvent(cancelEvent, context);
+ }
+ }
+
+ @Override
+ public void cancelSubscriptions(final List<DefaultSubscriptionBase> subscriptions, final List<SubscriptionBaseEvent> cancelEvents, final InternalCallContext context) {
+ synchronized (events) {
+ for (int i = 0; i < subscriptions.size(); i++) {
+ cancelSubscription(subscriptions.get(i), cancelEvents.get(i), context, 0);
+ }
+ }
+ }
+
+ @Override
+ public void changePlan(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> changeEvents, final InternalCallContext context) {
+ synchronized (events) {
+ cancelNextChangeEvent(subscription.getId());
+ cancelNextPhaseEvent(subscription.getId(), context);
+ events.addAll(changeEvents);
+ for (final SubscriptionBaseEvent cur : changeEvents) {
+ recordFutureNotificationFromTransaction(null, cur.getEffectiveDate(), new SubscriptionNotificationKey(cur.getId()), context);
+ }
+ }
+ }
+
+ private void insertEvent(final SubscriptionBaseEvent event, final InternalCallContext context) {
+ synchronized (events) {
+ events.add(event);
+ recordFutureNotificationFromTransaction(null, event.getEffectiveDate(), new SubscriptionNotificationKey(event.getId()), context);
+ }
+ }
+
+ private void cancelNextPhaseEvent(final UUID subscriptionId, final InternalTenantContext context) {
+ final SubscriptionBase curSubscription = getSubscriptionFromId(subscriptionId, context);
+ if (curSubscription.getCurrentPhase() == null ||
+ curSubscription.getCurrentPhase().getDuration().getUnit() == TimeUnit.UNLIMITED) {
+ return;
+ }
+
+ synchronized (events) {
+
+ final Iterator<SubscriptionBaseEvent> it = events.descendingIterator();
+ while (it.hasNext()) {
+ final SubscriptionBaseEvent cur = it.next();
+ if (cur.getSubscriptionId() != subscriptionId) {
+ continue;
+ }
+ if (cur.getType() == EventType.PHASE &&
+ cur.getEffectiveDate().isAfter(clock.getUTCNow())) {
+ cur.deactivate();
+ break;
+ }
+
+ }
+ }
+ }
+
+ private void cancelNextChangeEvent(final UUID subscriptionId) {
+
+ synchronized (events) {
+
+ final Iterator<SubscriptionBaseEvent> it = events.descendingIterator();
+ while (it.hasNext()) {
+ final SubscriptionBaseEvent cur = it.next();
+ if (cur.getSubscriptionId() != subscriptionId) {
+ continue;
+ }
+ if (cur.getType() == EventType.API_USER &&
+ ApiEventType.CHANGE == ((ApiEvent) cur).getEventType() &&
+ cur.getEffectiveDate().isAfter(clock.getUTCNow())) {
+ cur.deactivate();
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void uncancelSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> uncancelEvents,
+ final InternalCallContext context) {
+
+ synchronized (events) {
+ boolean foundCancel = false;
+ final Iterator<SubscriptionBaseEvent> it = events.descendingIterator();
+ while (it.hasNext()) {
+ final SubscriptionBaseEvent cur = it.next();
+ if (cur.getSubscriptionId() != subscription.getId()) {
+ continue;
+ }
+ if (cur.getType() == EventType.API_USER &&
+ ((ApiEvent) cur).getEventType() == ApiEventType.CANCEL) {
+ cur.deactivate();
+ foundCancel = true;
+ break;
+ }
+ }
+ if (foundCancel) {
+ for (final SubscriptionBaseEvent cur : uncancelEvents) {
+ insertEvent(cur, context);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void migrate(final UUID accountId, final AccountMigrationData accountData, final InternalCallContext context) {
+ synchronized (events) {
+
+ for (final BundleMigrationData curBundle : accountData.getData()) {
+ final DefaultSubscriptionBaseBundle bundleData = curBundle.getData();
+ for (final SubscriptionMigrationData curSubscription : curBundle.getSubscriptions()) {
+ final DefaultSubscriptionBase subData = curSubscription.getData();
+ for (final SubscriptionBaseEvent curEvent : curSubscription.getInitialEvents()) {
+ events.add(curEvent);
+ recordFutureNotificationFromTransaction(null, curEvent.getEffectiveDate(),
+ new SubscriptionNotificationKey(curEvent.getId()), context);
+
+ }
+ subscriptions.add(subData);
+ }
+ bundles.add(bundleData);
+ }
+ }
+ }
+
+ @Override
+ public SubscriptionBaseEvent getEventById(final UUID eventId, final InternalTenantContext context) {
+ synchronized (events) {
+ for (final SubscriptionBaseEvent cur : events) {
+ if (cur.getId().equals(eventId)) {
+ return cur;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void recordFutureNotificationFromTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> transactionalDao, final DateTime effectiveDate,
+ final NotificationEvent notificationKey, final InternalCallContext context) {
+ try {
+ final NotificationQueue subscriptionEventQueue = notificationQueueService.getNotificationQueue(DefaultSubscriptionBaseService.SUBSCRIPTION_SERVICE_NAME,
+ DefaultSubscriptionBaseService.NOTIFICATION_QUEUE_NAME);
+ subscriptionEventQueue.recordFutureNotificationFromTransaction(null, effectiveDate, notificationKey, context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
+ } catch (NoSuchNotificationQueue e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Map<UUID, List<SubscriptionBaseEvent>> getEventsForBundle(final UUID bundleId, final InternalTenantContext context) {
+ return null;
+ }
+
+ @Override
+ public void repair(final UUID accountId, final UUID bundleId, final List<SubscriptionDataRepair> inRepair,
+ final InternalCallContext context) {
+ }
+
+ @Override
+ public void transfer(final UUID srcAccountId, final UUID destAccountId, final BundleMigrationData data,
+ final List<TransferCancelData> transferCancelData, final InternalCallContext fromContext,
+ final InternalCallContext toContext) {
+ }
+
+ @Override
+ public void updateBundleExternalKey(final UUID bundleId, final String externalKey, final InternalCallContext context) {
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoSql.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoSql.java
new file mode 100644
index 0000000..4dd3357
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoSql.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.engine.dao;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.clock.Clock;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.subscription.engine.addon.AddonUtils;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import com.google.inject.Inject;
+
+public class MockSubscriptionDaoSql extends DefaultSubscriptionDao {
+
+ @Inject
+ public MockSubscriptionDaoSql(final IDBI dbi, final Clock clock, final AddonUtils addonUtils, final NotificationQueueService notificationQueueService,
+ final PersistentBus eventBus, final CatalogService catalogService, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ super(dbi, clock, addonUtils, notificationQueueService, eventBus, catalogService, cacheControllerDispatcher, nonEntityDao);
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModule.java b/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModule.java
new file mode 100644
index 0000000..ff38995
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModule.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.glue;
+
+import org.mockito.Mockito;
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.catalog.glue.CatalogModule;
+import org.killbill.billing.subscription.DefaultSubscriptionTestInitializer;
+import org.killbill.billing.subscription.SubscriptionTestInitializer;
+import org.killbill.billing.subscription.api.user.TestSubscriptionHelper;
+import org.killbill.billing.util.glue.CacheModule;
+import org.killbill.billing.util.glue.CallContextModule;
+
+public class TestDefaultSubscriptionModule extends DefaultSubscriptionModule {
+
+ public TestDefaultSubscriptionModule(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ install(new CatalogModule(configSource));
+ install(new CallContextModule());
+ install(new CacheModule(configSource));
+
+ bind(AccountUserApi.class).toInstance(Mockito.mock(AccountUserApi.class));
+
+ bind(TestSubscriptionHelper.class).asEagerSingleton();
+ bind(TestApiListener.class).asEagerSingleton();
+ bind(SubscriptionTestInitializer.class).to(DefaultSubscriptionTestInitializer.class).asEagerSingleton();
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleNoDB.java b/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleNoDB.java
new file mode 100644
index 0000000..e98712b
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleNoDB.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+import org.killbill.billing.mock.glue.MockNonEntityDaoModule;
+import org.killbill.notificationq.MockNotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueConfig;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.subscription.api.timeline.RepairSubscriptionLifecycleDao;
+import org.killbill.billing.subscription.engine.dao.MockSubscriptionDaoMemory;
+import org.killbill.billing.subscription.engine.dao.RepairSubscriptionDao;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.util.bus.InMemoryBusModule;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.name.Names;
+
+public class TestDefaultSubscriptionModuleNoDB extends TestDefaultSubscriptionModule {
+
+ public TestDefaultSubscriptionModuleNoDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void installSubscriptionDao() {
+ bind(SubscriptionDao.class).to(MockSubscriptionDaoMemory.class).asEagerSingleton();
+ bind(SubscriptionDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class);
+ bind(RepairSubscriptionLifecycleDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class);
+ bind(RepairSubscriptionDao.class).asEagerSingleton();
+ }
+
+ private void installNotificationQueue() {
+ bind(NotificationQueueService.class).to(MockNotificationQueueService.class).asEagerSingleton();
+ configureNotificationQueueConfig();
+ }
+
+ protected void configureNotificationQueueConfig() {
+ final NotificationQueueConfig config = new ConfigurationObjectFactory(configSource).buildWithReplacements(NotificationQueueConfig.class,
+ ImmutableMap.<String, String>of("instanceName", "main"));
+ bind(NotificationQueueConfig.class).toInstance(config);
+ }
+
+ @Override
+ protected void configure() {
+
+ install(new GuicyKillbillTestNoDBModule());
+
+ super.configure();
+
+ install(new InMemoryBusModule(configSource));
+ installNotificationQueue();
+
+ install(new MockNonEntityDaoModule());
+
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleWithEmbeddedDB.java b/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleWithEmbeddedDB.java
new file mode 100644
index 0000000..7cb0187
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/glue/TestDefaultSubscriptionModuleWithEmbeddedDB.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
+import org.killbill.billing.subscription.api.timeline.RepairSubscriptionLifecycleDao;
+import org.killbill.billing.subscription.engine.dao.MockSubscriptionDaoSql;
+import org.killbill.billing.subscription.engine.dao.RepairSubscriptionDao;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.util.glue.BusModule;
+import org.killbill.billing.util.glue.CustomFieldModule;
+import org.killbill.billing.util.glue.MetricsModule;
+import org.killbill.billing.util.glue.NonEntityDaoModule;
+import org.killbill.billing.util.glue.NotificationQueueModule;
+
+import com.google.inject.name.Names;
+
+public class TestDefaultSubscriptionModuleWithEmbeddedDB extends TestDefaultSubscriptionModule {
+
+ public TestDefaultSubscriptionModuleWithEmbeddedDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void installSubscriptionDao() {
+ bind(SubscriptionDao.class).to(MockSubscriptionDaoSql.class).asEagerSingleton();
+ bind(SubscriptionDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class);
+ bind(RepairSubscriptionLifecycleDao.class).annotatedWith(Names.named(REPAIR_NAMED)).to(RepairSubscriptionDao.class);
+ bind(RepairSubscriptionDao.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+
+ install(new GuicyKillbillTestWithEmbeddedDBModule());
+
+ install(new NonEntityDaoModule());
+
+ //installDBI();
+
+ install(new NotificationQueueModule(configSource));
+ install(new CustomFieldModule());
+ install(new MetricsModule());
+ install(new BusModule(configSource));
+ super.configure();
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestInitializer.java b/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestInitializer.java
new file mode 100644
index 0000000..aa0b39c
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestInitializer.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription;
+
+import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseService;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+public interface SubscriptionTestInitializer {
+
+ public Catalog initCatalog(final CatalogService catalogService) throws Exception;
+
+ public AccountData initAccountData();
+
+ public SubscriptionBaseBundle initBundle(final SubscriptionBaseInternalApi subscriptionApi, final InternalCallContext callContext) throws Exception;
+
+ public void startTestFamework(final TestApiListener testListener,
+ final ClockMock clock,
+ final BusService busService,
+ final SubscriptionBaseService subscriptionBaseService) throws Exception;
+
+ public void stopTestFramework(final TestApiListener testListener,
+ final BusService busService,
+ final SubscriptionBaseService subscriptionBaseService) throws Exception;
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteNoDB.java b/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteNoDB.java
new file mode 100644
index 0000000..5a96a8c
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteNoDB.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription;
+
+import java.net.URL;
+
+import javax.inject.Inject;
+
+import org.mockito.Mockito;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.subscription.api.SubscriptionBaseService;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineApi;
+import org.killbill.billing.subscription.api.transfer.SubscriptionBaseTransferApi;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.TestSubscriptionHelper;
+import org.killbill.billing.subscription.engine.dao.MockSubscriptionDaoMemory;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.glue.TestDefaultSubscriptionModuleNoDB;
+import org.killbill.billing.util.config.SubscriptionConfig;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+
+public class SubscriptionTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ protected static final Logger log = LoggerFactory.getLogger(SubscriptionTestSuiteNoDB.class);
+
+ @Inject
+ protected SubscriptionBaseService subscriptionBaseService;
+ @Inject
+ protected SubscriptionBaseInternalApi subscriptionInternalApi;
+ @Inject
+ protected SubscriptionBaseTransferApi transferApi;
+
+ @Inject
+ protected SubscriptionBaseMigrationApi migrationApi;
+ @Inject
+ protected SubscriptionBaseTimelineApi repairApi;
+
+ @Inject
+ protected CatalogService catalogService;
+ @Inject
+ protected SubscriptionConfig config;
+ @Inject
+ protected SubscriptionDao dao;
+ @Inject
+ protected ClockMock clock;
+ @Inject
+ protected BusService busService;
+
+ @Inject
+ protected IDBI idbi;
+
+ @Inject
+ protected TestSubscriptionHelper testUtil;
+ @Inject
+ protected TestApiListener testListener;
+
+ @Inject
+ protected SubscriptionTestInitializer subscriptionTestInitializer;
+
+ protected Catalog catalog;
+ protected AccountData accountData;
+ protected SubscriptionBaseBundle bundle;
+
+ private void loadSystemPropertiesFromClasspath(final String resource) {
+ final URL url = DefaultSubscriptionTestInitializer.class.getResource(resource);
+ Assert.assertNotNull(url);
+
+ configSource.merge(url);
+ }
+
+ @BeforeClass(groups = "fast")
+ public void beforeClass() throws Exception {
+ loadSystemPropertiesFromClasspath("/subscription.properties");
+
+ final Injector g = Guice.createInjector(Stage.PRODUCTION, new TestDefaultSubscriptionModuleNoDB(configSource));
+ g.injectMembers(this);
+
+ // For TestApiListener#isCompleted
+ Mockito.doReturn(0L).when(idbi).withHandle(Mockito.<HandleCallback<Long>>any());
+ }
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+
+ // CLEANUP ALL DB TABLES OR IN MEMORY STRUCTURES
+ ((MockSubscriptionDaoMemory) dao).reset();
+
+ subscriptionTestInitializer.startTestFamework(testListener, clock, busService, subscriptionBaseService);
+
+ this.catalog = subscriptionTestInitializer.initCatalog(catalogService);
+ this.accountData = subscriptionTestInitializer.initAccountData();
+ this.bundle = subscriptionTestInitializer.initBundle(subscriptionInternalApi, internalCallContext);
+ }
+
+ @AfterMethod(groups = "fast")
+ public void afterMethod() throws Exception {
+ subscriptionTestInitializer.stopTestFramework(testListener, busService, subscriptionBaseService);
+ }
+
+ protected void assertListenerStatus() {
+ testListener.assertListenerStatus();
+ }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteWithEmbeddedDB.java b/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..148aa1c
--- /dev/null
+++ b/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription;
+
+import java.net.URL;
+
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogService;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseService;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineApi;
+import org.killbill.billing.subscription.api.transfer.SubscriptionBaseTransferApi;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.TestSubscriptionHelper;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.glue.TestDefaultSubscriptionModuleWithEmbeddedDB;
+import org.killbill.billing.util.config.SubscriptionConfig;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+
+public class SubscriptionTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
+
+ protected static final Logger log = LoggerFactory.getLogger(SubscriptionTestSuiteWithEmbeddedDB.class);
+
+ public static final Long DELAY = 10000L;
+
+ @Inject
+ protected SubscriptionBaseService subscriptionBaseService;
+ @Inject
+ protected SubscriptionBaseInternalApi subscriptionInternalApi;
+ @Inject
+ protected SubscriptionBaseTransferApi transferApi;
+
+ @Inject
+ protected SubscriptionBaseMigrationApi migrationApi;
+ @Inject
+ protected SubscriptionBaseTimelineApi repairApi;
+
+ @Inject
+ protected CatalogService catalogService;
+ @Inject
+ protected SubscriptionConfig config;
+ @Inject
+ protected SubscriptionDao dao;
+ @Inject
+ protected ClockMock clock;
+ @Inject
+ protected BusService busService;
+
+ @Inject
+ protected TestSubscriptionHelper testUtil;
+ @Inject
+ protected TestApiListener testListener;
+ @Inject
+ protected SubscriptionTestInitializer subscriptionTestInitializer;
+
+ protected Catalog catalog;
+ protected AccountData accountData;
+ protected SubscriptionBaseBundle bundle;
+
+ private void loadSystemPropertiesFromClasspath(final String resource) {
+ final URL url = DefaultSubscriptionTestInitializer.class.getResource(resource);
+ Assert.assertNotNull(url);
+
+ configSource.merge(url);
+ }
+
+ @BeforeClass(groups = "slow")
+ public void beforeClass() throws Exception {
+ loadSystemPropertiesFromClasspath("/subscription.properties");
+
+ final Injector g = Guice.createInjector(Stage.PRODUCTION, new TestDefaultSubscriptionModuleWithEmbeddedDB(configSource));
+ g.injectMembers(this);
+ }
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ subscriptionTestInitializer.startTestFamework(testListener, clock, busService, subscriptionBaseService);
+
+ this.catalog = subscriptionTestInitializer.initCatalog(catalogService);
+ this.accountData = subscriptionTestInitializer.initAccountData();
+ this.bundle = subscriptionTestInitializer.initBundle(subscriptionInternalApi, internalCallContext);
+
+ // Make sure we start with a clean state
+ assertListenerStatus();
+ }
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ // Make sure we finish in a clean state
+ assertListenerStatus();
+
+ subscriptionTestInitializer.stopTestFramework(testListener, busService, subscriptionBaseService);
+ }
+
+ protected void assertListenerStatus() {
+ testListener.assertListenerStatus();
+ }
+}
diff --git a/subscription/src/test/resources/localtest.xml b/subscription/src/test/resources/localtest.xml
index 5670608..fd85f87 100644
--- a/subscription/src/test/resources/localtest.xml
+++ b/subscription/src/test/resources/localtest.xml
@@ -9,7 +9,7 @@
</run>
</groups>
<classes>
- <class name="com.ning.billing.subscription.api.user.TestUserApiChangePlanMemory"/>
+ <class name="org.killbill.billing.subscription.api.user.TestUserApiChangePlanMemory"/>
</classes>
</test>
<test name="Cancel">
@@ -20,7 +20,7 @@
</run>
</groups>
<classes>
- <class name="com.ning.billing.subscription.api.user.TestUserApiCancelMemory"/>
+ <class name="org.killbill.billing.subscription.api.user.TestUserApiCancelMemory"/>
</classes>
</test>
<test name="Create">
@@ -31,7 +31,7 @@
</run>
</groups>
<classes>
- <class name="com.ning.billing.subscription.api.user.TestUserApiCreateMemory"/>
+ <class name="org.killbill.billing.subscription.api.user.TestUserApiCreateMemory"/>
</classes>
</test>
</suite>
diff --git a/subscription/src/test/resources/subscription.properties b/subscription/src/test/resources/subscription.properties
index b1d5ad6..9695ca4 100644
--- a/subscription/src/test/resources/subscription.properties
+++ b/subscription/src/test/resources/subscription.properties
@@ -1,5 +1,5 @@
-killbill.catalog.uri=file:src/test/resources/testInput.xml
-killbill.billing.persistent.bus.main.sleep=100
-killbill.billing.persistent.bus.main.nbThreads=1
-killbill.billing.persistent.bus.main.claimed=1
+org.killbill.catalog.uri=file:src/test/resources/testInput.xml
+org.killbill.persistent.bus.main.sleep=100
+org.killbill.persistent.bus.main.nbThreads=1
+org.killbill.persistent.bus.main.claimed=1
user.timezone=UTC
diff --git a/subscription/src/test/resources/testng-default.xml b/subscription/src/test/resources/testng-default.xml
index ba23c4a..a21f7b1 100644
--- a/subscription/src/test/resources/testng-default.xml
+++ b/subscription/src/test/resources/testng-default.xml
@@ -9,7 +9,7 @@
</run>
</groups>
<classes>
- <class name="com.ning.billing.subscription.api.user.TestUserApiCancelMemory"/>
+ <class name="org.killbill.billing.subscription.api.user.TestUserApiCancelMemory"/>
</classes>
</test>
<test name="Create">
@@ -20,7 +20,7 @@
</run>
</groups>
<classes>
- <class name="com.ning.billing.subscription.api.user.TestUserApiCreateMemory"/>
+ <class name="org.killbill.billing.subscription.api.user.TestUserApiCreateMemory"/>
</classes>
</test>
</suite>
diff --git a/subscription/src/test/resources/testng-localtest.xml b/subscription/src/test/resources/testng-localtest.xml
index e9dd6ea..87fb894 100644
--- a/subscription/src/test/resources/testng-localtest.xml
+++ b/subscription/src/test/resources/testng-localtest.xml
@@ -9,7 +9,7 @@
</run>
</groups>
<classes>
- <class name="com.ning.billing.subscription.api.user.TestUserApiChangePlanMemory"/>
+ <class name="org.killbill.billing.subscription.api.user.TestUserApiChangePlanMemory"/>
</classes>
</test>
</suite>
tenant/pom.xml 66(+23 -43)
diff --git a/tenant/pom.xml b/tenant/pom.xml
index 8ec6759..4cfdf52 100644
--- a/tenant/pom.xml
+++ b/tenant/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-tenant</artifactId>
@@ -41,71 +41,51 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.antlr</groupId>
+ <artifactId>stringtemplate</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.shiro</groupId>
+ <artifactId>shiro-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.antlr</groupId>
- <artifactId>stringtemplate</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-core</artifactId>
- </dependency>
- <dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenant.java b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenant.java
new file mode 100644
index 0000000..354546f
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenant.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.api;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.tenant.dao.TenantModelDao;
+import org.killbill.billing.entity.EntityBase;
+
+public class DefaultTenant extends EntityBase implements Tenant {
+
+ private final String externalKey;
+ private final String apiKey;
+ // Decrypted (clear) secret
+ private final String apiSecret;
+
+ /**
+ * This call is used to create a tenant
+ *
+ * @param data TenantData new data for the tenant
+ */
+ public DefaultTenant(final TenantData data) {
+ this(UUID.randomUUID(), data);
+ }
+
+ /**
+ * This call is used to update an existing tenant
+ *
+ * @param id UUID id of the existing tenant to update
+ * @param data TenantData new data for the existing tenant
+ */
+ public DefaultTenant(final UUID id, final TenantData data) {
+ this(id, null, null, data.getExternalKey(), data.getApiKey(), data.getApiSecret());
+ }
+
+ public DefaultTenant(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate,
+ final String externalKey, final String apiKey, final String apiSecret) {
+ super(id, createdDate, updatedDate);
+ this.externalKey = externalKey;
+ this.apiKey = apiKey;
+ this.apiSecret = apiSecret;
+ }
+
+ public DefaultTenant(final TenantModelDao tenant) {
+ this(tenant.getId(), tenant.getCreatedDate(), tenant.getUpdatedDate(), tenant.getExternalKey(), tenant.getApiKey(),
+ tenant.getApiSecret());
+ }
+
+ @Override
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ @Override
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ @Override
+ public String getApiSecret() {
+ return apiSecret;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultTenant");
+ sb.append("{externalKey='").append(externalKey).append('\'');
+ sb.append(", apiKey='").append(apiKey).append('\'');
+ // Don't print the secret
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultTenant that = (DefaultTenant) o;
+
+ if (apiKey != null ? !apiKey.equals(that.apiKey) : that.apiKey != null) {
+ return false;
+ }
+ if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
+ return false;
+ }
+ if (apiSecret != null ? !apiSecret.equals(that.apiSecret) : that.apiSecret != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = externalKey != null ? externalKey.hashCode() : 0;
+ result = 31 * result + (apiKey != null ? apiKey.hashCode() : 0);
+ result = 31 * result + (apiSecret != null ? apiSecret.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantKV.java b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantKV.java
new file mode 100644
index 0000000..eca907d
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantKV.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.tenant.api;
+
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.entity.EntityBase;
+
+public class DefaultTenantKV extends EntityBase implements TenantKV {
+
+ private final String key;
+ private final String value;
+
+ public DefaultTenantKV(final UUID id, final String key, final String value, final DateTime createdDate, final DateTime updatedDate) {
+ super(id, createdDate, updatedDate);
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantService.java b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantService.java
new file mode 100644
index 0000000..88578f8
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantService.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.api;
+
+public class DefaultTenantService implements TenantService {
+
+ private static final String TENANT_SERVICE_NAME = "tenant-service";
+
+ @Override
+ public String getName() {
+ return TENANT_SERVICE_NAME;
+ }
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java b/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
new file mode 100644
index 0000000..f5781f7
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.api.user;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.tenant.api.DefaultTenant;
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.tenant.api.TenantApiException;
+import org.killbill.billing.tenant.api.TenantData;
+import org.killbill.billing.tenant.api.TenantUserApi;
+import org.killbill.billing.tenant.dao.TenantDao;
+import org.killbill.billing.tenant.dao.TenantModelDao;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+import com.google.inject.Inject;
+
+public class DefaultTenantUserApi implements TenantUserApi {
+
+ private final TenantDao tenantDao;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public DefaultTenantUserApi(final TenantDao tenantDao, final InternalCallContextFactory internalCallContextFactory) {
+ this.tenantDao = tenantDao;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public Tenant createTenant(final TenantData data, final CallContext context) throws TenantApiException {
+ final Tenant tenant = new DefaultTenant(data);
+
+ try {
+ tenantDao.create(new TenantModelDao(tenant), internalCallContextFactory.createInternalCallContext(context));
+ } catch (final TenantApiException e) {
+ throw new TenantApiException(e, ErrorCode.TENANT_CREATION_FAILED);
+ }
+
+ return tenant;
+ }
+
+ @Override
+ public Tenant getTenantByApiKey(final String key) throws TenantApiException {
+ final TenantModelDao tenant = tenantDao.getTenantByApiKey(key);
+ if (tenant == null) {
+ throw new TenantApiException(ErrorCode.TENANT_DOES_NOT_EXIST_FOR_API_KEY, key);
+ }
+ return new DefaultTenant(tenant);
+ }
+
+ @Override
+ public Tenant getTenantById(final UUID id) throws TenantApiException {
+ // TODO - API cleanup?
+ final TenantModelDao tenant = tenantDao.getById(id, new InternalTenantContext(null, null));
+ if (tenant == null) {
+ throw new TenantApiException(ErrorCode.TENANT_DOES_NOT_EXIST_FOR_ID, id);
+ }
+ return new DefaultTenant(tenant);
+ }
+
+ @Override
+ public List<String> getTenantValueForKey(final String key, final TenantContext context)
+ throws TenantApiException {
+ final InternalTenantContext internalContext = internalCallContextFactory.createInternalTenantContext(context);
+ return tenantDao.getTenantValueForKey(key, internalContext);
+ }
+
+ @Override
+ public void addTenantKeyValue(final String key, final String value, final CallContext context)
+ throws TenantApiException {
+
+ final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(context);
+ // TODO Figure out the exact verification if nay
+ /*
+ final Tenant tenant = tenantDao.getById(callcontext.getTenantId(), internalContext);
+ if (tenant == null) {
+ throw new TenantApiException(ErrorCode.TENANT_DOES_NOT_EXIST_FOR_ID, tenantId);
+ }
+ */
+ tenantDao.addTenantKeyValue(key, value, internalContext);
+ }
+
+ @Override
+ public void deleteTenantKey(final String key, final CallContext context)
+ throws TenantApiException {
+ /*
+ final Tenant tenant = tenantDao.getById(tenantId, new InternalTenantContext(null, null));
+ if (tenant == null) {
+ throw new TenantApiException(ErrorCode.TENANT_DOES_NOT_EXIST_FOR_ID, tenantId);
+ }
+ */
+ final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(context);
+ tenantDao.deleteTenantKey(key, internalContext);
+ }
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java
new file mode 100644
index 0000000..85350ef
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.SimpleAuthenticationInfo;
+import org.apache.shiro.codec.Base64;
+import org.apache.shiro.crypto.RandomNumberGenerator;
+import org.apache.shiro.crypto.SecureRandomNumberGenerator;
+import org.apache.shiro.crypto.hash.SimpleHash;
+import org.apache.shiro.util.ByteSource;
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.tenant.api.TenantApiException;
+import org.killbill.billing.tenant.security.KillbillCredentialsMatcher;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.dao.EntityDaoBase;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+public class DefaultTenantDao extends EntityDaoBase<TenantModelDao, Tenant, TenantApiException> implements TenantDao {
+
+ private final RandomNumberGenerator rng = new SecureRandomNumberGenerator();
+
+ @Inject
+ public DefaultTenantDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao), TenantSqlDao.class);
+ }
+
+ @Override
+ protected TenantApiException generateAlreadyExistsException(final TenantModelDao entity, final InternalCallContext context) {
+ return new TenantApiException(ErrorCode.TENANT_ALREADY_EXISTS, entity.getExternalKey());
+ }
+
+ @Override
+ public TenantModelDao getTenantByApiKey(final String apiKey) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<TenantModelDao>() {
+ @Override
+ public TenantModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(TenantSqlDao.class).getByApiKey(apiKey);
+ }
+ });
+ }
+
+ @Override
+ public void create(final TenantModelDao entity, final InternalCallContext context) throws TenantApiException {
+ // Create the salt and password
+ final ByteSource salt = rng.nextBytes();
+ // Hash the plain-text password with the random salt and multiple iterations and then Base64-encode the value (requires less space than Hex)
+ final String hashedPasswordBase64 = new SimpleHash(KillbillCredentialsMatcher.HASH_ALGORITHM_NAME,
+ entity.getApiSecret(), salt, KillbillCredentialsMatcher.HASH_ITERATIONS).toBase64();
+
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final TenantModelDao tenantModelDaoWithSecret = new TenantModelDao(entity.getId(), context.getCreatedDate(), context.getUpdatedDate(),
+ entity.getExternalKey(), entity.getApiKey(),
+ hashedPasswordBase64, salt.toBase64());
+ entitySqlDaoWrapperFactory.become(TenantSqlDao.class).create(tenantModelDaoWithSecret, context);
+ return null;
+ }
+ });
+ }
+
+ @VisibleForTesting
+ AuthenticationInfo getAuthenticationInfoForTenant(final UUID id) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<AuthenticationInfo>() {
+ @Override
+ public AuthenticationInfo inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final TenantModelDao tenantModelDao = entitySqlDaoWrapperFactory.become(TenantSqlDao.class).getSecrets(id.toString());
+
+ final SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(tenantModelDao.getApiKey(), tenantModelDao.getApiSecret().toCharArray(), getClass().getSimpleName());
+ authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(Base64.decode(tenantModelDao.getApiSalt())));
+
+ return authenticationInfo;
+ }
+ });
+ }
+
+ @Override
+ public List<String> getTenantValueForKey(final String key, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<String>>() {
+ @Override
+ public List<String> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final List<TenantKVModelDao> tenantKV = entitySqlDaoWrapperFactory.become(TenantKVSqlDao.class).getTenantValueForKey(key, context);
+ return ImmutableList.copyOf(Collections2.transform(tenantKV, new Function<TenantKVModelDao, String>() {
+ @Override
+ public String apply(final TenantKVModelDao in) {
+ return in.getTenantValue();
+ }
+ }));
+ }
+ });
+ }
+
+ @Override
+ public void addTenantKeyValue(final String key, final String value, final InternalCallContext context) {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final TenantKVModelDao tenantKVModelDao = new TenantKVModelDao(UUID.randomUUID(), context.getCreatedDate(), context.getUpdatedDate(), key, value);
+ entitySqlDaoWrapperFactory.become(TenantKVSqlDao.class).create(tenantKVModelDao, context);
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public void deleteTenantKey(final String key, final InternalCallContext context) {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final List<TenantKVModelDao> tenantKVs = entitySqlDaoWrapperFactory.become(TenantKVSqlDao.class).getTenantValueForKey(key, context);
+ for (TenantKVModelDao cur : tenantKVs) {
+ if (cur.getTenantKey().equals(key)) {
+ entitySqlDaoWrapperFactory.become(TenantKVSqlDao.class).markTenantKeyAsDeleted(cur.getId().toString(), context);
+ }
+ }
+ return null;
+ }
+ });
+ }
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantDao.java
new file mode 100644
index 0000000..84fbcde
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantDao.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.dao;
+
+import java.util.List;
+
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.tenant.api.TenantApiException;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.dao.EntityDao;
+
+public interface TenantDao extends EntityDao<TenantModelDao, Tenant, TenantApiException> {
+
+ public TenantModelDao getTenantByApiKey(final String key);
+
+ public List<String> getTenantValueForKey(final String key, final InternalTenantContext context);
+
+ public void addTenantKeyValue(final String key, final String value, final InternalCallContext context);
+
+ public void deleteTenantKey(final String key, final InternalCallContext context);
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantKVModelDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantKVModelDao.java
new file mode 100644
index 0000000..04e72a0
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantKVModelDao.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.dao;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.tenant.api.TenantKV;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class TenantKVModelDao extends EntityBase implements EntityModelDao<TenantKV> {
+
+ private String tenantKey;
+ private String tenantValue;
+
+ private Boolean isActive;
+
+ public TenantKVModelDao() { /* For the DAO mapper */ }
+
+ public TenantKVModelDao(final UUID id, final DateTime createdDate, final DateTime updatedDate, final String key, final String value) {
+ super(id, createdDate, updatedDate);
+ this.tenantKey = key;
+ this.tenantValue = value;
+ this.isActive = true;
+ }
+
+ public String getTenantKey() {
+ return tenantKey;
+ }
+
+ public String getTenantValue() {
+ return tenantValue;
+ }
+
+ public Boolean getIsActive() {
+ return isActive;
+ }
+
+ public void setTenantKey(final String tenantKey) {
+ this.tenantKey = tenantKey;
+ }
+
+ public void setTenantValue(final String tenantValue) {
+ this.tenantValue = tenantValue;
+ }
+
+ public void setIsActive(final Boolean isActive) {
+ this.isActive = isActive;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TenantKVModelDao");
+ sb.append("{key='").append(tenantKey).append('\'');
+ sb.append(", value='").append(tenantValue).append('\'');
+ sb.append(", isActive=").append(isActive);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final TenantKVModelDao that = (TenantKVModelDao) o;
+
+ if (isActive != null ? !isActive.equals(that.isActive) : that.isActive != null) {
+ return false;
+ }
+ if (tenantKey != null ? !tenantKey.equals(that.tenantKey) : that.tenantKey != null) {
+ return false;
+ }
+ if (tenantValue != null ? !tenantValue.equals(that.tenantValue) : that.tenantValue != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (tenantKey != null ? tenantKey.hashCode() : 0);
+ result = 31 * result + (tenantValue != null ? tenantValue.hashCode() : 0);
+ result = 31 * result + (isActive != null ? isActive.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.TENANT_KVS;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return null;
+ }
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantKVSqlDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantKVSqlDao.java
new file mode 100644
index 0000000..bdfed51
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantKVSqlDao.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.dao;
+
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.tenant.api.TenantKV;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface TenantKVSqlDao extends EntitySqlDao<TenantKVModelDao, TenantKV> {
+
+ @SqlQuery
+ public List<TenantKVModelDao> getTenantValueForKey(@Bind("tenantKey") final String key,
+ @BindBean final InternalTenantContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.DELETE)
+ public void markTenantKeyAsDeleted(@Bind("id")final String id,
+ @BindBean final InternalCallContext context);
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantModelDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantModelDao.java
new file mode 100644
index 0000000..5cb998e
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantModelDao.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.dao;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class TenantModelDao extends EntityBase implements EntityModelDao<Tenant> {
+
+ private String externalKey;
+ private String apiKey;
+ private String apiSecret;
+ private String apiSalt;
+
+ public TenantModelDao() { /* For the DAO mapper */ }
+
+ public TenantModelDao(final UUID id, final DateTime createdDate, final DateTime updatedDate, final String externalKey,
+ final String apiKey, final String apiSecret, final String apiSalt) {
+ super(id, createdDate, updatedDate);
+ this.externalKey = externalKey;
+ this.apiKey = apiKey;
+ this.apiSecret = apiSecret;
+ this.apiSalt = apiSalt;
+ }
+
+ public TenantModelDao(final Tenant tenant) {
+ this(tenant.getId(), tenant.getCreatedDate(), tenant.getUpdatedDate(), tenant.getExternalKey(),
+ tenant.getApiKey(), tenant.getApiSecret(), null);
+ }
+
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ public String getApiSecret() {
+ return apiSecret;
+ }
+
+ public String getApiSalt() {
+ return apiSalt;
+ }
+
+ public void setExternalKey(final String externalKey) {
+ this.externalKey = externalKey;
+ }
+
+ public void setApiKey(final String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public void setApiSecret(final String apiSecret) {
+ this.apiSecret = apiSecret;
+ }
+
+ public void setApiSalt(final String apiSalt) {
+ this.apiSalt = apiSalt;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TenantModelDao");
+ sb.append("{externalKey='").append(externalKey).append('\'');
+ sb.append(", apiKey='").append(apiKey).append('\'');
+ sb.append(", apiSecret='").append(apiSecret).append('\'');
+ sb.append(", apiSalt='").append(apiSalt).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final TenantModelDao that = (TenantModelDao) o;
+
+ if (apiKey != null ? !apiKey.equals(that.apiKey) : that.apiKey != null) {
+ return false;
+ }
+ if (apiSalt != null ? !apiSalt.equals(that.apiSalt) : that.apiSalt != null) {
+ return false;
+ }
+ if (apiSecret != null ? !apiSecret.equals(that.apiSecret) : that.apiSecret != null) {
+ return false;
+ }
+ if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+ result = 31 * result + (apiKey != null ? apiKey.hashCode() : 0);
+ result = 31 * result + (apiSecret != null ? apiSecret.hashCode() : 0);
+ result = 31 * result + (apiSalt != null ? apiSalt.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.TENANT;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return null;
+ }
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantSqlDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantSqlDao.java
new file mode 100644
index 0000000..1f71b00
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantSqlDao.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.dao;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface TenantSqlDao extends EntitySqlDao<TenantModelDao, Tenant> {
+
+ @SqlQuery
+ public TenantModelDao getByApiKey(@Bind("apiKey") final String apiKey);
+
+ @SqlQuery
+ public TenantModelDao getSecrets(@Bind("id") final String id);
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/glue/TenantModule.java b/tenant/src/main/java/org/killbill/billing/tenant/glue/TenantModule.java
new file mode 100644
index 0000000..1296cc4
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/glue/TenantModule.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.tenant.api.DefaultTenantService;
+import org.killbill.billing.tenant.api.TenantService;
+import org.killbill.billing.tenant.api.TenantUserApi;
+import org.killbill.billing.tenant.api.user.DefaultTenantUserApi;
+import org.killbill.billing.tenant.dao.DefaultTenantDao;
+import org.killbill.billing.tenant.dao.TenantDao;
+
+import com.google.inject.AbstractModule;
+
+public class TenantModule extends AbstractModule {
+
+ protected final ConfigSource configSource;
+
+ public TenantModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ private void installConfig() {
+ }
+
+ protected void installTenantDao() {
+ bind(TenantDao.class).to(DefaultTenantDao.class).asEagerSingleton();
+ }
+
+ protected void installTenantUserApi() {
+ bind(TenantUserApi.class).to(DefaultTenantUserApi.class).asEagerSingleton();
+ }
+
+ private void installTenantService() {
+ bind(TenantService.class).to(DefaultTenantService.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ installConfig();
+ installTenantDao();
+ installTenantService();
+ installTenantUserApi();
+ }
+}
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/security/KillbillCredentialsMatcher.java b/tenant/src/main/java/org/killbill/billing/tenant/security/KillbillCredentialsMatcher.java
new file mode 100644
index 0000000..f1ce2a2
--- /dev/null
+++ b/tenant/src/main/java/org/killbill/billing/tenant/security/KillbillCredentialsMatcher.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.security;
+
+import org.apache.shiro.authc.credential.CredentialsMatcher;
+import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
+import org.apache.shiro.crypto.hash.Sha512Hash;
+
+public class KillbillCredentialsMatcher {
+
+ public static final String KILLBILL_TENANT_HASH_ITERATIONS_PROPERTY = "org.killbill.server.multitenant.hash_iterations";
+
+ // See http://www.stormpath.com/blog/strong-password-hashing-apache-shiro and https://issues.apache.org/jira/browse/SHIRO-290
+ public static final String HASH_ALGORITHM_NAME = Sha512Hash.ALGORITHM_NAME;
+ public static final Integer HASH_ITERATIONS = Integer.parseInt(System.getProperty(KILLBILL_TENANT_HASH_ITERATIONS_PROPERTY, "200000"));
+
+ private KillbillCredentialsMatcher() {}
+
+ public static CredentialsMatcher getCredentialsMatcher() {
+ // This needs to be in sync with DefaultTenantDao
+ final HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(HASH_ALGORITHM_NAME);
+ // base64 encoding, not hex
+ credentialsMatcher.setStoredCredentialsHexEncoded(false);
+ credentialsMatcher.setHashIterations(HASH_ITERATIONS);
+
+ return credentialsMatcher;
+ }
+}
diff --git a/tenant/src/main/resources/org/killbill/billing/tenant/dao/TenantKVSqlDao.sql.stg b/tenant/src/main/resources/org/killbill/billing/tenant/dao/TenantKVSqlDao.sql.stg
new file mode 100644
index 0000000..c42eaca
--- /dev/null
+++ b/tenant/src/main/resources/org/killbill/billing/tenant/dao/TenantKVSqlDao.sql.stg
@@ -0,0 +1,48 @@
+group TenantKVSqlDao: EntitySqlDao;
+
+tableName() ::= "tenant_kvs"
+
+andCheckSoftDeletionWithComma(prefix) ::= "and <prefix>is_active"
+
+tableFields(prefix) ::= <<
+ <prefix>tenant_key
+, <prefix>tenant_value
+, <prefix>is_active
+, <prefix>created_date
+, <prefix>created_by
+, <prefix>updated_date
+, <prefix>updated_by
+>>
+
+tableValues() ::= <<
+ :tenantKey
+, :tenantValue
+, :isActive
+, :createdDate
+, :createdBy
+, :updatedDate
+, :updatedBy
+>>
+
+accountRecordIdFieldWithComma(prefix) ::= ""
+
+accountRecordIdValueWithComma() ::= ""
+
+
+getTenantValueForKey() ::= <<
+select
+ <allTableFields("t.")>
+from <tableName()> t
+where t.tenant_key = :tenantKey
+and t.is_active
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+markTenantKeyAsDeleted() ::= <<
+update <tableName()> t
+set t.is_active = 0
+where t.id = :id
+<AND_CHECK_TENANT("t.")>
+;
+>>
diff --git a/tenant/src/main/resources/org/killbill/billing/tenant/dao/TenantSqlDao.sql.stg b/tenant/src/main/resources/org/killbill/billing/tenant/dao/TenantSqlDao.sql.stg
new file mode 100644
index 0000000..56c8986
--- /dev/null
+++ b/tenant/src/main/resources/org/killbill/billing/tenant/dao/TenantSqlDao.sql.stg
@@ -0,0 +1,66 @@
+group TenantDaoSql: EntitySqlDao;
+
+tableName() ::= "tenants"
+
+/* Don't add api_secret and api_salt in these fields, we shouldn't need to retrieve them */
+tableFields(prefix) ::= <<
+ <prefix>external_key
+, <prefix>api_key
+, <prefix>created_date
+, <prefix>created_by
+, <prefix>updated_date
+, <prefix>updated_by
+>>
+
+tableValues() ::= <<
+ :externalKey
+, :apiKey
+, :createdDate
+, :createdBy
+, :updatedDate
+, :updatedBy
+>>
+
+/* No account_record_id field */
+accountRecordIdFieldWithComma(prefix) ::= ""
+accountRecordIdValueWithComma(prefix) ::= ""
+
+/* No tenant_record_id field */
+tenantRecordIdFieldWithComma(prefix) ::= ""
+tenantRecordIdValueWithComma(prefix) ::= ""
+CHECK_TENANT(prefix) ::= "1 = 1"
+
+/* Override default create call to include secrets */
+create() ::= <<
+insert into <tableName()> (
+ <idField()>
+, <tableFields()>
+, api_secret
+, api_salt
+)
+values (
+ <idValue()>
+, <tableValues()>
+, :apiSecret
+, :apiSalt
+)
+;
+>>
+
+getByApiKey() ::= <<
+select
+ <allTableFields("t.")>
+from <tableName()> t
+where api_key = :apiKey
+;
+>>
+
+getSecrets() ::= <<
+select
+ <allTableFields("t.")>
+, t.api_secret
+, t.api_salt
+from <tableName()> t
+where <idField("t.")> = <idValue()>
+;
+>>
diff --git a/tenant/src/main/resources/org/killbill/billing/tenant/ddl.sql b/tenant/src/main/resources/org/killbill/billing/tenant/ddl.sql
new file mode 100644
index 0000000..40d439a
--- /dev/null
+++ b/tenant/src/main/resources/org/killbill/billing/tenant/ddl.sql
@@ -0,0 +1,35 @@
+/*! SET storage_engine=INNODB */;
+
+DROP TABLE IF EXISTS tenants;
+CREATE TABLE tenants (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ external_key varchar(128) NULL,
+ api_key varchar(128) NULL,
+ api_secret varchar(128) NULL,
+ api_salt varchar(128) NULL,
+ created_date datetime NOT NULL,
+ created_by varchar(50) NOT NULL,
+ updated_date datetime DEFAULT NULL,
+ updated_by varchar(50) DEFAULT NULL,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX tenants_id ON tenants(id);
+CREATE UNIQUE INDEX tenants_api_key ON tenants(api_key);
+
+
+DROP TABLE IF EXISTS tenant_kvs;
+CREATE TABLE tenant_kvs (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ tenant_record_id int(11) unsigned default null,
+ tenant_key varchar(64) NOT NULL,
+ tenant_value varchar(1024) NOT NULL,
+ is_active bool DEFAULT 1,
+ created_date datetime NOT NULL,
+ created_by varchar(50) NOT NULL,
+ updated_date datetime DEFAULT NULL,
+ updated_by varchar(50) DEFAULT NULL,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX tenant_kvs_key ON tenant_kvs(tenant_key);
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java b/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java
new file mode 100644
index 0000000..48edb93
--- /dev/null
+++ b/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.tenant.TenantTestSuiteWithEmbeddedDb;
+import org.killbill.billing.tenant.api.DefaultTenant;
+import org.killbill.billing.tenant.security.KillbillCredentialsMatcher;
+
+public class TestDefaultTenantDao extends TenantTestSuiteWithEmbeddedDb {
+
+ @Test(groups = "slow")
+ public void testWeCanStoreAndMatchCredentials() throws Exception {
+ final DefaultTenant tenant = new DefaultTenant(UUID.randomUUID(), null, null, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), UUID.randomUUID().toString());
+ tenantDao.create(new TenantModelDao(tenant), internalCallContext);
+
+ // Verify we can retrieve it
+ Assert.assertEquals(tenantDao.getTenantByApiKey(tenant.getApiKey()).getId(), tenant.getId());
+
+ // Verify we can authenticate against it
+ final AuthenticationInfo authenticationInfo = tenantDao.getAuthenticationInfoForTenant(tenant.getId());
+
+ // Good combo
+ final AuthenticationToken goodToken = new UsernamePasswordToken(tenant.getApiKey(), tenant.getApiSecret());
+ Assert.assertTrue(KillbillCredentialsMatcher.getCredentialsMatcher().doCredentialsMatch(goodToken, authenticationInfo));
+
+ // Bad combo
+ final AuthenticationToken badToken = new UsernamePasswordToken(tenant.getApiKey(), tenant.getApiSecret() + "T");
+ Assert.assertFalse(KillbillCredentialsMatcher.getCredentialsMatcher().doCredentialsMatch(badToken, authenticationInfo));
+ }
+
+ @Test(groups = "slow")
+ public void testTenantKeyValue() throws Exception {
+ final DefaultTenant tenant = new DefaultTenant(UUID.randomUUID(), null, null, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), UUID.randomUUID().toString());
+ tenantDao.create(new TenantModelDao(tenant), internalCallContext);
+
+ tenantDao.addTenantKeyValue("TheKey", "TheValue", internalCallContext);
+
+ List<String> value = tenantDao.getTenantValueForKey("TheKey", internalCallContext);
+ Assert.assertEquals(value.size(), 1);
+ Assert.assertEquals(value.get(0), "TheValue");
+
+ tenantDao.addTenantKeyValue("TheKey", "TheSecondValue", internalCallContext);
+ value = tenantDao.getTenantValueForKey("TheKey", internalCallContext);
+ Assert.assertEquals(value.size(), 2);
+
+ tenantDao.deleteTenantKey("TheKey", internalCallContext);
+ value = tenantDao.getTenantValueForKey("TheKey", internalCallContext);
+ Assert.assertEquals(value.size(), 0);
+ }
+}
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModule.java b/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModule.java
new file mode 100644
index 0000000..3886451
--- /dev/null
+++ b/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModule.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.util.glue.CacheModule;
+import org.killbill.billing.util.glue.CallContextModule;
+
+public class TestTenantModule extends TenantModule {
+
+ public TestTenantModule(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+
+ install(new CacheModule(configSource));
+ install(new CallContextModule());
+ }
+}
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleNoDB.java b/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleNoDB.java
new file mode 100644
index 0000000..724211b
--- /dev/null
+++ b/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleNoDB.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+import org.killbill.billing.mock.glue.MockNonEntityDaoModule;
+
+public class TestTenantModuleNoDB extends TestTenantModule {
+
+ public TestTenantModuleNoDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ public void configure() {
+ super.configure();
+
+ install(new GuicyKillbillTestNoDBModule());
+ install(new MockNonEntityDaoModule());
+ }
+}
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java b/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java
new file mode 100644
index 0000000..3f05c33
--- /dev/null
+++ b/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
+import org.killbill.billing.util.glue.NonEntityDaoModule;
+
+public class TestTenantModuleWithEmbeddedDB extends TestTenantModule {
+
+ public TestTenantModuleWithEmbeddedDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ public void configure() {
+ super.configure();
+
+ install(new GuicyKillbillTestWithEmbeddedDBModule());
+ install(new NonEntityDaoModule());
+ }
+}
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteNoDB.java b/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteNoDB.java
new file mode 100644
index 0000000..dac6c5d
--- /dev/null
+++ b/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteNoDB.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant;
+
+import org.testng.annotations.BeforeClass;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.billing.tenant.glue.TestTenantModuleNoDB;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class TenantTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ @BeforeClass(groups = "fast")
+ protected void beforeClass() throws Exception {
+ final Injector injector = Guice.createInjector(new TestTenantModuleNoDB(configSource));
+ injector.injectMembers(this);
+ }
+}
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.java b/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.java
new file mode 100644
index 0000000..25ded5e
--- /dev/null
+++ b/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.tenant;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+import org.killbill.billing.tenant.dao.DefaultTenantDao;
+import org.killbill.billing.tenant.glue.TestTenantModuleWithEmbeddedDB;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+public class TenantTestSuiteWithEmbeddedDb extends GuicyKillbillTestSuiteWithEmbeddedDB {
+
+ @Inject
+ protected DefaultTenantDao tenantDao;
+
+ @BeforeClass(groups = "slow")
+ protected void beforeClass() throws Exception {
+ final Injector injector = Guice.createInjector(new TestTenantModuleWithEmbeddedDB(configSource));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ }
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() {
+ }
+}
usage/pom.xml 58(+19 -39)
diff --git a/usage/pom.xml b/usage/pom.xml
index a273359..c6b50ea 100644
--- a/usage/pom.xml
+++ b/usage/pom.xml
@@ -18,8 +18,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-usage</artifactId>
@@ -36,67 +36,47 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.antlr</groupId>
+ <artifactId>stringtemplate</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-internal-api</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
+ <groupId>org.kill-bill.billing</groupId>
<artifactId>killbill-util</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
+ <groupId>org.kill-bill.commons</groupId>
<artifactId>killbill-clock</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.antlr</groupId>
- <artifactId>stringtemplate</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.jdbi</groupId>
- <artifactId>jdbi</artifactId>
- </dependency>
- <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
diff --git a/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultRolledUpUsage.java b/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultRolledUpUsage.java
new file mode 100644
index 0000000..3681acf
--- /dev/null
+++ b/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultRolledUpUsage.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage.api.user;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.usage.api.RolledUpUsage;
+import org.killbill.billing.usage.dao.RolledUpUsageModelDao;
+
+public class DefaultRolledUpUsage implements RolledUpUsage {
+
+ private final UUID subscriptionId;
+ private final String unitType;
+ private final DateTime startTime;
+ private final DateTime endTime;
+ private final BigDecimal amount;
+
+ public DefaultRolledUpUsage(final UUID subscriptionId, final String unitType, final DateTime startTime, final DateTime endTime,
+ final BigDecimal amount) {
+ this.subscriptionId = subscriptionId;
+ this.unitType = unitType;
+ this.startTime = startTime;
+ this.endTime = endTime;
+ this.amount = amount;
+ }
+
+ public DefaultRolledUpUsage(final RolledUpUsageModelDao usageForSubscription) {
+ this(usageForSubscription.getSubscriptionId(), usageForSubscription.getUnitType(), usageForSubscription.getStartTime(),
+ usageForSubscription.getEndTime(), usageForSubscription.getAmount());
+ }
+
+ @Override
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ public String getUnitType() {
+ return unitType;
+ }
+
+ @Override
+ public DateTime getStartTime() {
+ return startTime;
+ }
+
+ @Override
+ public DateTime getEndTime() {
+ return endTime;
+ }
+
+ @Override
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultRolledUpUsage");
+ sb.append("{subscriptionId=").append(subscriptionId);
+ sb.append(", unitType='").append(unitType).append('\'');
+ sb.append(", startTime=").append(startTime);
+ sb.append(", endTime=").append(endTime);
+ sb.append(", amount=").append(amount);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultRolledUpUsage that = (DefaultRolledUpUsage) o;
+
+ if (amount != null ? !amount.equals(that.amount) : that.amount != null) {
+ return false;
+ }
+ if (endTime != null ? !endTime.equals(that.endTime) : that.endTime != null) {
+ return false;
+ }
+ if (unitType != null ? !unitType.equals(that.unitType) : that.unitType != null) {
+ return false;
+ }
+ if (startTime != null ? !startTime.equals(that.startTime) : that.startTime != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = subscriptionId != null ? subscriptionId.hashCode() : 0;
+ result = 31 * result + (unitType != null ? unitType.hashCode() : 0);
+ result = 31 * result + (startTime != null ? startTime.hashCode() : 0);
+ result = 31 * result + (endTime != null ? endTime.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java b/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java
new file mode 100644
index 0000000..2ae8f34
--- /dev/null
+++ b/usage/src/main/java/org/killbill/billing/usage/api/user/DefaultUsageUserApi.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage.api.user;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.usage.api.RolledUpUsage;
+import org.killbill.billing.usage.api.UsageUserApi;
+import org.killbill.billing.usage.dao.RolledUpUsageDao;
+import org.killbill.billing.usage.dao.RolledUpUsageModelDao;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+public class DefaultUsageUserApi implements UsageUserApi {
+
+ private final RolledUpUsageDao rolledUpUsageDao;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public DefaultUsageUserApi(final RolledUpUsageDao rolledUpUsageDao,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.rolledUpUsageDao = rolledUpUsageDao;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public void recordRolledUpUsage(final UUID subscriptionId, final String unitType, final DateTime startTime, final DateTime endTime,
+ final BigDecimal amount, final CallContext context) {
+ final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(subscriptionId, ObjectType.SUBSCRIPTION, context);
+ rolledUpUsageDao.record(subscriptionId, unitType, startTime, endTime, amount, internalCallContext);
+ }
+
+ @Override
+ public RolledUpUsage getUsageForSubscription(final UUID subscriptionId, final TenantContext context) {
+ final RolledUpUsageModelDao usageForSubscription = rolledUpUsageDao.getUsageForSubscription(subscriptionId, internalCallContextFactory.createInternalTenantContext(context));
+ return new DefaultRolledUpUsage(usageForSubscription);
+ }
+}
diff --git a/usage/src/main/java/org/killbill/billing/usage/dao/DefaultRolledUpUsageDao.java b/usage/src/main/java/org/killbill/billing/usage/dao/DefaultRolledUpUsageDao.java
new file mode 100644
index 0000000..353a799
--- /dev/null
+++ b/usage/src/main/java/org/killbill/billing/usage/dao/DefaultRolledUpUsageDao.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage.dao;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+public class DefaultRolledUpUsageDao implements RolledUpUsageDao {
+
+ private final RolledUpUsageSqlDao rolledUpUsageSqlDao;
+
+ @Inject
+ public DefaultRolledUpUsageDao(final IDBI dbi) {
+ this.rolledUpUsageSqlDao = dbi.onDemand(RolledUpUsageSqlDao.class);
+ }
+
+ @Override
+ public void record(final UUID subscriptionId, final String unitType, final DateTime startTime, final DateTime endTime,
+ final BigDecimal amount, final InternalCallContext context) {
+ final RolledUpUsageModelDao rolledUpUsageModelDao = new RolledUpUsageModelDao(subscriptionId, unitType, startTime,
+ endTime, amount
+ );
+ rolledUpUsageSqlDao.create(rolledUpUsageModelDao, context);
+ }
+
+ @Override
+ public RolledUpUsageModelDao getUsageForSubscription(final UUID subscriptionId, final InternalTenantContext context) {
+ return rolledUpUsageSqlDao.getUsageForSubscription(subscriptionId, context);
+ }
+}
diff --git a/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageDao.java b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageDao.java
new file mode 100644
index 0000000..e0b90a1
--- /dev/null
+++ b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageDao.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage.dao;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+public interface RolledUpUsageDao {
+
+ void record(UUID subscriptionId, String unitType, DateTime startTime,
+ DateTime endTime, BigDecimal amount, InternalCallContext context);
+
+ RolledUpUsageModelDao getUsageForSubscription(UUID subscriptionId, InternalTenantContext context);
+}
diff --git a/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageModelDao.java b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageModelDao.java
new file mode 100644
index 0000000..acffd5b
--- /dev/null
+++ b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageModelDao.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage.dao;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+public class RolledUpUsageModelDao {
+
+ private UUID id;
+ private UUID subscriptionId;
+ private String unitType;
+ private DateTime startTime;
+ private DateTime endTime;
+ private BigDecimal amount;
+
+ public RolledUpUsageModelDao() { /* For the DAO mapper */ }
+
+ public RolledUpUsageModelDao(final UUID subscriptionId, final String unitType, final DateTime startTime,
+ final DateTime endTime, final BigDecimal amount) {
+ this.id = UUID.randomUUID();
+ this.subscriptionId = subscriptionId;
+ this.unitType = unitType;
+ this.startTime = startTime;
+ this.endTime = endTime;
+ this.amount = amount;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ public String getUnitType() {
+ return unitType;
+ }
+
+ public DateTime getStartTime() {
+ return startTime;
+ }
+
+ public DateTime getEndTime() {
+ return endTime;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public void setId(final UUID id) {
+ this.id = id;
+ }
+
+ public void setSubscriptionId(final UUID subscriptionId) {
+ this.subscriptionId = subscriptionId;
+ }
+
+ public void setUnitType(final String unitType) {
+ this.unitType = unitType;
+ }
+
+ public void setStartTime(final DateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public void setEndTime(final DateTime endTime) {
+ this.endTime = endTime;
+ }
+
+ public void setAmount(final BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("RolledUpUsageModelDao");
+ sb.append("{id=").append(id);
+ sb.append(", subscriptionId=").append(subscriptionId);
+ sb.append(", unitType='").append(unitType).append('\'');
+ sb.append(", startTime=").append(startTime);
+ sb.append(", endTime=").append(endTime);
+ sb.append(", amount=").append(amount);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final RolledUpUsageModelDao that = (RolledUpUsageModelDao) o;
+
+ if (amount != null ? !amount.equals(that.amount) : that.amount != null) {
+ return false;
+ }
+ if (endTime != null ? !endTime.equals(that.endTime) : that.endTime != null) {
+ return false;
+ }
+ if (id != null ? !id.equals(that.id) : that.id != null) {
+ return false;
+ }
+ if (unitType != null ? !unitType.equals(that.unitType) : that.unitType != null) {
+ return false;
+ }
+ if (startTime != null ? !startTime.equals(that.startTime) : that.startTime != null) {
+ return false;
+ }
+ if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + (subscriptionId != null ? subscriptionId.hashCode() : 0);
+ result = 31 * result + (unitType != null ? unitType.hashCode() : 0);
+ result = 31 * result + (startTime != null ? startTime.hashCode() : 0);
+ result = 31 * result + (endTime != null ? endTime.hashCode() : 0);
+ result = 31 * result + (amount != null ? amount.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.java b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.java
new file mode 100644
index 0000000..29042ca
--- /dev/null
+++ b/usage/src/main/java/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage.dao;
+
+import java.util.UUID;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.callcontext.InternalTenantContextBinder;
+
+@UseStringTemplate3StatementLocator()
+public interface RolledUpUsageSqlDao {
+
+ @SqlUpdate
+ public void create(@BindBean RolledUpUsageModelDao rolledUpUsage,
+ @InternalTenantContextBinder final InternalCallContext context);
+
+ @SqlQuery
+ public RolledUpUsageModelDao getUsageForSubscription(@Bind("subscriptionId") final UUID subscriptionId,
+ @InternalTenantContextBinder final InternalTenantContext context);
+}
diff --git a/usage/src/main/java/org/killbill/billing/usage/glue/UsageModule.java b/usage/src/main/java/org/killbill/billing/usage/glue/UsageModule.java
new file mode 100644
index 0000000..64755b0
--- /dev/null
+++ b/usage/src/main/java/org/killbill/billing/usage/glue/UsageModule.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.usage.api.UsageUserApi;
+import org.killbill.billing.usage.api.user.DefaultUsageUserApi;
+import org.killbill.billing.usage.dao.DefaultRolledUpUsageDao;
+import org.killbill.billing.usage.dao.RolledUpUsageDao;
+
+import com.google.inject.AbstractModule;
+
+public class UsageModule extends AbstractModule {
+
+ protected final ConfigSource configSource;
+
+ public UsageModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ protected void installRolledUpUsageDao() {
+ bind(RolledUpUsageDao.class).to(DefaultRolledUpUsageDao.class).asEagerSingleton();
+ }
+
+ protected void installUsageUserApi() {
+ bind(UsageUserApi.class).to(DefaultUsageUserApi.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ installRolledUpUsageDao();
+ installUsageUserApi();
+ }
+}
diff --git a/usage/src/main/resources/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.sql.stg b/usage/src/main/resources/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.sql.stg
new file mode 100644
index 0000000..e028757
--- /dev/null
+++ b/usage/src/main/resources/org/killbill/billing/usage/dao/RolledUpUsageSqlDao.sql.stg
@@ -0,0 +1,51 @@
+group RolledUpUsageSqlDao;
+
+tableName() ::= "usage"
+
+tableFields(prefix) ::= <<
+ <prefix>id
+, <prefix>subscription_id
+, <prefix>unit_type
+, <prefix>start_time
+, <prefix>end_time
+, <prefix>amount
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>account_record_id
+, <prefix>tenant_record_id
+>>
+
+tableValues() ::= <<
+ :id
+, :subscriptionId
+, :unitType
+, :startTime
+, :endTime
+, :amount
+, :userName
+, :createdDate
+, :accountRecordId
+, :tenantRecordId
+>>
+
+CHECK_TENANT(prefix) ::= "<prefix>tenant_record_id = :tenantRecordId"
+AND_CHECK_TENANT(prefix) ::= "and <CHECK_TENANT(prefix)>"
+
+create() ::= <<
+insert into <tableName()> (
+ <tableFields()>
+)
+values (
+ <tableValues()>
+)
+;
+>>
+
+getUsageForSubscription() ::= <<
+select
+ <tableFields("t.")>
+from <tableName()> t
+where subscription_id = :subscriptionId
+<AND_CHECK_TENANT()>
+;
+>>
diff --git a/usage/src/main/resources/org/killbill/billing/usage/ddl.sql b/usage/src/main/resources/org/killbill/billing/usage/ddl.sql
new file mode 100644
index 0000000..aa1bd96
--- /dev/null
+++ b/usage/src/main/resources/org/killbill/billing/usage/ddl.sql
@@ -0,0 +1,20 @@
+/*! SET storage_engine=INNODB */;
+
+DROP TABLE IF EXISTS rolled_up_usage;
+CREATE TABLE rolled_up_usage (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ subscription_id char(36),
+ unit_type varchar(50),
+ start_date date NOT NULL,
+ end_date date,
+ amount numeric(10,10) NOT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX rolled_up_usage_id ON rolled_up_usage(id);
+CREATE INDEX rolled_up_usage_subscription_id ON rolled_up_usage(subscription_id ASC);
+CREATE INDEX rolled_up_usage_tenant_account_record_id ON rolled_up_usage(tenant_record_id, account_record_id);
diff --git a/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModule.java b/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModule.java
new file mode 100644
index 0000000..c03237d
--- /dev/null
+++ b/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage.glue;
+
+import org.skife.config.ConfigSource;
+
+public class TestUsageModule extends UsageModule {
+
+ public TestUsageModule(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ }
+}
diff --git a/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModuleNoDB.java b/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModuleNoDB.java
new file mode 100644
index 0000000..c2eee78
--- /dev/null
+++ b/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModuleNoDB.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+
+public class TestUsageModuleNoDB extends TestUsageModule {
+
+ public TestUsageModuleNoDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ public void configure() {
+ super.configure();
+
+ install(new GuicyKillbillTestNoDBModule());
+ }
+}
diff --git a/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModuleWithEmbeddedDB.java b/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModuleWithEmbeddedDB.java
new file mode 100644
index 0000000..6faae57
--- /dev/null
+++ b/usage/src/test/java/org/killbill/billing/usage/glue/TestUsageModuleWithEmbeddedDB.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
+
+public class TestUsageModuleWithEmbeddedDB extends TestUsageModule {
+
+ public TestUsageModuleWithEmbeddedDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ public void configure() {
+ super.configure();
+
+ install(new GuicyKillbillTestWithEmbeddedDBModule());
+ }
+}
diff --git a/usage/src/test/java/org/killbill/billing/usage/UsageTestSuiteNoDB.java b/usage/src/test/java/org/killbill/billing/usage/UsageTestSuiteNoDB.java
new file mode 100644
index 0000000..2b58cea
--- /dev/null
+++ b/usage/src/test/java/org/killbill/billing/usage/UsageTestSuiteNoDB.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.billing.usage.glue.TestUsageModuleNoDB;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class UsageTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ @BeforeClass(groups = "fast")
+ protected void beforeClass() throws Exception {
+ final Injector injector = Guice.createInjector(new TestUsageModuleNoDB(configSource));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() {
+ }
+
+ @AfterMethod(groups = "fast")
+ public void afterMethod() {
+ }
+}
diff --git a/usage/src/test/java/org/killbill/billing/usage/UsageTestSuiteWithEmbeddedDB.java b/usage/src/test/java/org/killbill/billing/usage/UsageTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..9126e99
--- /dev/null
+++ b/usage/src/test/java/org/killbill/billing/usage/UsageTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.usage;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+import org.killbill.billing.usage.glue.TestUsageModuleWithEmbeddedDB;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class UsageTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
+
+ @BeforeClass(groups = "slow")
+ protected void beforeClass() throws Exception {
+ final Injector injector = Guice.createInjector(new TestUsageModuleWithEmbeddedDB(configSource));
+ injector.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ }
+
+ @AfterMethod(groups = "fast")
+ public void afterMethod() {
+ }
+}
util/pom.xml 123(+57 -66)
diff --git a/util/pom.xml b/util/pom.xml
index f8efff6..d9ddbf4 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -11,8 +11,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>killbill</artifactId>
- <groupId>com.ning.billing</groupId>
- <version>0.9.0-SNAPSHOT</version>
+ <groupId>org.kill-bill.billing</groupId>
+ <version>0.9.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-util</artifactId>
@@ -56,11 +56,6 @@
<artifactId>guice-multibindings</artifactId>
</dependency>
<dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
@@ -74,59 +69,10 @@
<artifactId>c3p0</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-api</artifactId>
- </dependency>
- <dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-internal-api</artifactId>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-clock</artifactId>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-clock</artifactId>
- <type>test-jar</type>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-embeddeddb</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-locker</artifactId>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-queue</artifactId>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.commons</groupId>
- <artifactId>killbill-queue</artifactId>
- <type>test-jar</type>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.plugin</groupId>
- <artifactId>killbill-plugin-api-notification</artifactId>
- </dependency>
- <dependency>
- <groupId>com.ning.billing.plugin</groupId>
- <artifactId>killbill-plugin-api-payment</artifactId>
- </dependency>
- <dependency>
<groupId>com.samskivert</groupId>
<artifactId>jmustache</artifactId>
</dependency>
<dependency>
- <groupId>com.yammer.metrics</groupId>
- <artifactId>metrics-core</artifactId>
- </dependency>
- <dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<scope>provided</scope>
@@ -141,16 +87,6 @@
<scope>test</scope>
</dependency>
<dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-mxj-db-files</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<type>jar</type>
@@ -185,6 +121,61 @@
<version>0.9</version>
</dependency>
<dependency>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing</groupId>
+ <artifactId>killbill-internal-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing.plugin</groupId>
+ <artifactId>killbill-plugin-api-notification</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.billing.plugin</groupId>
+ <artifactId>killbill-plugin-api-payment</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-clock</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-clock</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-embeddeddb-common</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-embeddeddb-h2</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-embeddeddb-mysql</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-locker</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-queue</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kill-bill.commons</groupId>
+ <artifactId>killbill-queue</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
diff --git a/util/src/main/java/org/killbill/billing/util/audit/api/DefaultAuditUserApi.java b/util/src/main/java/org/killbill/billing/util/audit/api/DefaultAuditUserApi.java
new file mode 100644
index 0000000..9c035a3
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/audit/api/DefaultAuditUserApi.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.api.AuditLevel;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+import org.killbill.billing.util.audit.AccountAuditLogsForObjectType;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.audit.DefaultAccountAuditLogs;
+import org.killbill.billing.util.audit.DefaultAccountAuditLogsForObjectType;
+import org.killbill.billing.util.audit.dao.AuditDao;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.dao.TableName;
+
+import com.google.common.collect.ImmutableList;
+
+public class DefaultAuditUserApi implements AuditUserApi {
+
+ private final AuditDao auditDao;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public DefaultAuditUserApi(final AuditDao auditDao, final InternalCallContextFactory internalCallContextFactory) {
+ this.auditDao = auditDao;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public AccountAuditLogs getAccountAuditLogs(final UUID accountId, final AuditLevel auditLevel, final TenantContext tenantContext) {
+ // Optimization - bail early
+ if (AuditLevel.NONE.equals(auditLevel)) {
+ return new DefaultAccountAuditLogs(accountId);
+ }
+
+ return auditDao.getAuditLogsForAccountRecordId(auditLevel, internalCallContextFactory.createInternalTenantContext(accountId, tenantContext));
+ }
+
+ @Override
+ public AccountAuditLogsForObjectType getAccountAuditLogs(final UUID accountId, final ObjectType objectType, final AuditLevel auditLevel, final TenantContext tenantContext) {
+ // Optimization - bail early
+ if (AuditLevel.NONE.equals(auditLevel)) {
+ return new DefaultAccountAuditLogsForObjectType(auditLevel);
+ }
+
+ final TableName tableName = getTableNameFromObjectType(objectType);
+ if (tableName == null) {
+ return new DefaultAccountAuditLogsForObjectType(auditLevel);
+ }
+
+ return auditDao.getAuditLogsForAccountRecordId(tableName, auditLevel, internalCallContextFactory.createInternalTenantContext(accountId, tenantContext));
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogs(final UUID objectId, final ObjectType objectType, final AuditLevel auditLevel, final TenantContext context) {
+ // Optimization - bail early
+ if (AuditLevel.NONE.equals(auditLevel)) {
+ return ImmutableList.<AuditLog>of();
+ }
+
+ final TableName tableName = getTableNameFromObjectType(objectType);
+ if (tableName == null) {
+ return ImmutableList.<AuditLog>of();
+ }
+
+ return auditDao.getAuditLogsForId(tableName, objectId, auditLevel, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ private TableName getTableNameFromObjectType(final ObjectType objectType) {
+ for (final TableName tableName : TableName.values()) {
+ if (objectType.equals(tableName.getObjectType())) {
+ return tableName;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/audit/dao/AuditDao.java b/util/src/main/java/org/killbill/billing/util/audit/dao/AuditDao.java
new file mode 100644
index 0000000..6f19906
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/audit/dao/AuditDao.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.api.AuditLevel;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.audit.DefaultAccountAuditLogs;
+import org.killbill.billing.util.audit.DefaultAccountAuditLogsForObjectType;
+import org.killbill.billing.util.dao.TableName;
+
+public interface AuditDao {
+
+ // Make sure to consume all or call close() when done to release the connection
+ public DefaultAccountAuditLogs getAuditLogsForAccountRecordId(AuditLevel auditLevel, InternalTenantContext context);
+
+ // Make sure to consume all or call close() when done to release the connection
+ public DefaultAccountAuditLogsForObjectType getAuditLogsForAccountRecordId(TableName tableName, AuditLevel auditLevel, InternalTenantContext context);
+
+ public List<AuditLog> getAuditLogsForId(TableName tableName, UUID objectId, AuditLevel auditLevel, InternalTenantContext context);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/audit/dao/AuditLogModelDao.java b/util/src/main/java/org/killbill/billing/util/audit/dao/AuditLogModelDao.java
new file mode 100644
index 0000000..7ce819b
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/audit/dao/AuditLogModelDao.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit.dao;
+
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.dao.EntityAudit;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class AuditLogModelDao extends EntityAudit implements EntityModelDao<AuditLog> {
+
+ private final CallContext callContext;
+
+ public AuditLogModelDao(final EntityAudit entityAudit, final CallContext callContext) {
+ super(entityAudit.getId(), entityAudit.getTableName(), entityAudit.getTargetRecordId(), entityAudit.getChangeType(), entityAudit.getCreatedDate());
+ this.callContext = callContext;
+ }
+
+ public CallContext getCallContext() {
+ return callContext;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("AuditLogModelDao{");
+ sb.append("callContext=").append(callContext);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final AuditLogModelDao that = (AuditLogModelDao) o;
+
+ if (callContext != null ? !callContext.equals(that.callContext) : that.callContext != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (callContext != null ? callContext.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/audit/dao/DefaultAuditDao.java b/util/src/main/java/org/killbill/billing/util/audit/dao/DefaultAuditDao.java
new file mode 100644
index 0000000..5977c7e
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/audit/dao/DefaultAuditDao.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit.dao;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.util.api.AuditLevel;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.audit.DefaultAccountAuditLogs;
+import org.killbill.billing.util.audit.DefaultAccountAuditLogsForObjectType;
+import org.killbill.billing.util.audit.DefaultAuditLog;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.dao.NonEntitySqlDao;
+import org.killbill.billing.util.dao.RecordIdIdMappings;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Lists;
+
+public class DefaultAuditDao implements AuditDao {
+
+ private final NonEntitySqlDao nonEntitySqlDao;
+ private final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
+
+ @Inject
+ public DefaultAuditDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ this.nonEntitySqlDao = dbi.onDemand(NonEntitySqlDao.class);
+ this.transactionalSqlDao = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao);
+ }
+
+ @Override
+ public DefaultAccountAuditLogs getAuditLogsForAccountRecordId(final AuditLevel auditLevel, final InternalTenantContext context) {
+ final UUID accountId = nonEntitySqlDao.getIdFromObject(context.getAccountRecordId(), TableName.ACCOUNT.getTableName());
+
+ // Lazy evaluate records to minimize the memory footprint (these can yield a lot of results)
+ // We usually always want to wrap our queries in an EntitySqlDaoTransactionWrapper... except here.
+ // Since we want to stream the results out, we don't want to auto-commit when this method returns.
+ final EntitySqlDao auditSqlDao = transactionalSqlDao.onDemand(EntitySqlDao.class);
+ final Iterator<AuditLogModelDao> auditLogsForAccountRecordId = auditSqlDao.getAuditLogsForAccountRecordId(context);
+ final Iterator<AuditLog> allAuditLogs = buildAuditLogsFromModelDao(auditLogsForAccountRecordId, context);
+
+ return new DefaultAccountAuditLogs(accountId, auditLevel, allAuditLogs);
+ }
+
+ @Override
+ public DefaultAccountAuditLogsForObjectType getAuditLogsForAccountRecordId(final TableName tableName, final AuditLevel auditLevel, final InternalTenantContext context) {
+ final String actualTableName;
+ if (tableName.hasHistoryTable()) {
+ actualTableName = tableName.getHistoryTableName().name(); // upper cased
+ } else {
+ actualTableName = tableName.getTableName();
+ }
+
+ // Lazy evaluate records to minimize the memory footprint (these can yield a lot of results)
+ // We usually always want to wrap our queries in an EntitySqlDaoTransactionWrapper... except here.
+ // Since we want to stream the results out, we don't want to auto-commit when this method returns.
+ final EntitySqlDao auditSqlDao = transactionalSqlDao.onDemand(EntitySqlDao.class);
+ final Iterator<AuditLogModelDao> auditLogsForTableNameAndAccountRecordId = auditSqlDao.getAuditLogsForTableNameAndAccountRecordId(actualTableName, context);
+ final Iterator<AuditLog> allAuditLogs = buildAuditLogsFromModelDao(auditLogsForTableNameAndAccountRecordId, context);
+
+ return new DefaultAccountAuditLogsForObjectType(auditLevel, allAuditLogs);
+ }
+
+ private Iterator<AuditLog> buildAuditLogsFromModelDao(final Iterator<AuditLogModelDao> auditLogsForAccountRecordId, final InternalTenantContext tenantContext) {
+ final Map<TableName, Map<Long, UUID>> recordIdIdsCache = new HashMap<TableName, Map<Long, UUID>>();
+ final Map<TableName, Map<Long, UUID>> historyRecordIdIdsCache = new HashMap<TableName, Map<Long, UUID>>();
+ return Iterators.<AuditLogModelDao, AuditLog>transform(auditLogsForAccountRecordId,
+ new Function<AuditLogModelDao, AuditLog>() {
+ @Override
+ public AuditLog apply(final AuditLogModelDao input) {
+ // If input is for e.g. TAG_DEFINITION_HISTORY, retrieve TAG_DEFINITIONS
+ // For tables without history, e.g. TENANT, originalTableNameForHistoryTableName will be null
+ final TableName originalTableNameForHistoryTableName = findTableNameForHistoryTableName(input.getTableName());
+
+ final ObjectType objectType;
+ final UUID auditedEntityId;
+ if (originalTableNameForHistoryTableName != null) {
+ // input point to a history entry
+ objectType = originalTableNameForHistoryTableName.getObjectType();
+
+ if (historyRecordIdIdsCache.get(originalTableNameForHistoryTableName) == null) {
+ if (TableName.ACCOUNT.equals(originalTableNameForHistoryTableName)) {
+ final Iterable<RecordIdIdMappings> mappings = nonEntitySqlDao.getHistoryRecordIdIdMappingsForAccountsTable(originalTableNameForHistoryTableName.getTableName(),
+ input.getTableName().getTableName(),
+ tenantContext);
+ historyRecordIdIdsCache.put(originalTableNameForHistoryTableName, RecordIdIdMappings.toMap(mappings));
+ } else if (TableName.TAG_DEFINITIONS.equals(originalTableNameForHistoryTableName)) {
+ final Iterable<RecordIdIdMappings> mappings = nonEntitySqlDao.getHistoryRecordIdIdMappingsForTablesWithoutAccountRecordId(originalTableNameForHistoryTableName.getTableName(),
+ input.getTableName().getTableName(),
+ tenantContext);
+ historyRecordIdIdsCache.put(originalTableNameForHistoryTableName, RecordIdIdMappings.toMap(mappings));
+ } else {
+ final Iterable<RecordIdIdMappings> mappings = nonEntitySqlDao.getHistoryRecordIdIdMappings(originalTableNameForHistoryTableName.getTableName(),
+ input.getTableName().getTableName(),
+ tenantContext);
+ historyRecordIdIdsCache.put(originalTableNameForHistoryTableName, RecordIdIdMappings.toMap(mappings));
+
+ }
+ }
+
+ auditedEntityId = historyRecordIdIdsCache.get(originalTableNameForHistoryTableName).get(input.getTargetRecordId());
+ } else {
+ objectType = input.getTableName().getObjectType();
+
+ if (recordIdIdsCache.get(input.getTableName()) == null) {
+ final Iterable<RecordIdIdMappings> mappings = nonEntitySqlDao.getRecordIdIdMappings(input.getTableName().getTableName(),
+ tenantContext);
+ recordIdIdsCache.put(input.getTableName(), RecordIdIdMappings.toMap(mappings));
+ }
+
+ auditedEntityId = recordIdIdsCache.get(input.getTableName()).get(input.getTargetRecordId());
+ }
+
+ return new DefaultAuditLog(input, objectType, auditedEntityId);
+ }
+
+ private TableName findTableNameForHistoryTableName(final TableName historyTableName) {
+ for (final TableName tableName : TableName.values()) {
+ if (historyTableName.equals(tableName.getHistoryTableName())) {
+ return tableName;
+ }
+ }
+
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForId(final TableName tableName, final UUID objectId, final AuditLevel auditLevel, final InternalTenantContext context) {
+ if (tableName.hasHistoryTable()) {
+ return doGetAuditLogsViaHistoryForId(tableName, objectId, auditLevel, context);
+ } else {
+ return doGetAuditLogsForId(tableName, objectId, auditLevel, context);
+ }
+ }
+
+ private List<AuditLog> doGetAuditLogsForId(final TableName tableName, final UUID objectId, final AuditLevel auditLevel, final InternalTenantContext context) {
+ final Long recordId = nonEntitySqlDao.getRecordIdFromObject(objectId.toString(), tableName.getTableName());
+ if (recordId == null) {
+ return ImmutableList.<AuditLog>of();
+ } else {
+ return getAuditLogsForRecordId(tableName, objectId, recordId, auditLevel, context);
+ }
+ }
+
+ private List<AuditLog> doGetAuditLogsViaHistoryForId(final TableName tableName, final UUID objectId, final AuditLevel auditLevel, final InternalTenantContext context) {
+ final TableName historyTableName = tableName.getHistoryTableName();
+ if (historyTableName == null) {
+ throw new IllegalStateException("History table shouldn't be null for " + tableName);
+ }
+
+ final Long targetRecordId = nonEntitySqlDao.getRecordIdFromObject(objectId.toString(), tableName.getTableName());
+ final List<AuditLog> allAuditLogs = transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<AuditLog>>() {
+ @Override
+ public List<AuditLog> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final List<AuditLogModelDao> auditLogsViaHistoryForTargetRecordId = entitySqlDaoWrapperFactory.become(EntitySqlDao.class).getAuditLogsViaHistoryForTargetRecordId(historyTableName.name(),
+ historyTableName.getTableName().toLowerCase(),
+ targetRecordId,
+ context);
+ return buildAuditLogsFromModelDao(auditLogsViaHistoryForTargetRecordId, tableName.getObjectType(), objectId);
+ }
+ });
+ return filterAuditLogs(auditLevel, allAuditLogs);
+ }
+
+ private List<AuditLog> getAuditLogsForRecordId(final TableName tableName, final UUID auditedEntityId, final Long targetRecordId, final AuditLevel auditLevel, final InternalTenantContext context) {
+ final List<AuditLog> allAuditLogs = transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<AuditLog>>() {
+ @Override
+ public List<AuditLog> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final List<AuditLogModelDao> auditLogsForTargetRecordId = entitySqlDaoWrapperFactory.become(EntitySqlDao.class).getAuditLogsForTargetRecordId(tableName.name(),
+ targetRecordId,
+ context);
+ return buildAuditLogsFromModelDao(auditLogsForTargetRecordId, tableName.getObjectType(), auditedEntityId);
+ }
+ });
+ return filterAuditLogs(auditLevel, allAuditLogs);
+ }
+
+ private List<AuditLog> buildAuditLogsFromModelDao(final List<AuditLogModelDao> auditLogsForAccountRecordId, final ObjectType objectType, final UUID auditedEntityId) {
+ return Lists.<AuditLogModelDao, AuditLog>transform(auditLogsForAccountRecordId,
+ new Function<AuditLogModelDao, AuditLog>() {
+ @Override
+ public AuditLog apply(final AuditLogModelDao input) {
+ return new DefaultAuditLog(input, objectType, auditedEntityId);
+ }
+ });
+ }
+
+ private List<AuditLog> filterAuditLogs(final AuditLevel auditLevel, final List<AuditLog> auditLogs) {
+ // TODO Do the filtering in the query
+ if (AuditLevel.FULL.equals(auditLevel)) {
+ return auditLogs;
+ } else if (AuditLevel.MINIMAL.equals(auditLevel) && !auditLogs.isEmpty()) {
+ if (ChangeType.INSERT.equals(auditLogs.get(0).getChangeType())) {
+ return ImmutableList.<AuditLog>of(auditLogs.get(0));
+ } else {
+ // We may be coming here via the history code path - only a single mapped history record id
+ // will be for the initial INSERT
+ return ImmutableList.<AuditLog>of();
+ }
+ } else if (AuditLevel.NONE.equals(auditLevel)) {
+ return ImmutableList.<AuditLog>of();
+ } else {
+ return auditLogs;
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/audit/DefaultAccountAuditLogs.java b/util/src/main/java/org/killbill/billing/util/audit/DefaultAccountAuditLogs.java
new file mode 100644
index 0000000..85bd2da
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/audit/DefaultAccountAuditLogs.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.api.AuditLevel;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+
+public class DefaultAccountAuditLogs implements AccountAuditLogs {
+
+ private final UUID accountId;
+ private final AuditLevel auditLevel;
+ private final Collection<AuditLog> accountAuditLogs;
+
+ private final Map<ObjectType, DefaultAccountAuditLogsForObjectType> auditLogsCache = new HashMap<ObjectType, DefaultAccountAuditLogsForObjectType>();
+
+ public DefaultAccountAuditLogs(final UUID accountId) {
+ this(accountId, AuditLevel.NONE, Iterators.<AuditLog>emptyIterator());
+ }
+
+ public DefaultAccountAuditLogs(final UUID accountId, final AuditLevel auditLevel, final Iterator<AuditLog> accountAuditLogsOrderedByTableName) {
+ this.accountId = accountId;
+ this.auditLevel = auditLevel;
+ // TODO pierre - lame, we should be smarter to avoid loading all entries in memory. It's a bit tricky though...
+ this.accountAuditLogs = ImmutableList.<AuditLog>copyOf(accountAuditLogsOrderedByTableName);
+ }
+
+ public void close() {
+ // Make sure to go through the results to close the connection
+ // no-op for now, see TODO above
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForAccount() {
+ return getAuditLogs(ObjectType.ACCOUNT).getAuditLogs(accountId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForBundle(final UUID bundleId) {
+ return getAuditLogs(ObjectType.BUNDLE).getAuditLogs(bundleId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForSubscription(final UUID subscriptionId) {
+ return getAuditLogs(ObjectType.SUBSCRIPTION).getAuditLogs(subscriptionId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForSubscriptionEvent(final UUID subscriptionEventId) {
+ return getAuditLogs(ObjectType.SUBSCRIPTION_EVENT).getAuditLogs(subscriptionEventId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForInvoice(final UUID invoiceId) {
+ return getAuditLogs(ObjectType.INVOICE).getAuditLogs(invoiceId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForInvoiceItem(final UUID invoiceItemId) {
+ return getAuditLogs(ObjectType.INVOICE_ITEM).getAuditLogs(invoiceItemId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForPayment(final UUID paymentId) {
+ return getAuditLogs(ObjectType.PAYMENT).getAuditLogs(paymentId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForPaymentMethod(final UUID paymentMethodId) {
+ return getAuditLogs(ObjectType.PAYMENT_METHOD).getAuditLogs(paymentMethodId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForRefund(final UUID refundId) {
+ return getAuditLogs(ObjectType.REFUND).getAuditLogs(refundId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForChargeback(final UUID chargebackId) {
+ return getAuditLogs(ObjectType.INVOICE_PAYMENT).getAuditLogs(chargebackId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForBlockingState(final UUID blockingStateId) {
+ return getAuditLogs(ObjectType.BLOCKING_STATES).getAuditLogs(blockingStateId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForInvoicePayment(final UUID invoicePaymentId) {
+ return getAuditLogs(ObjectType.INVOICE_PAYMENT).getAuditLogs(invoicePaymentId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForTag(final UUID tagId) {
+ return getAuditLogs(ObjectType.TAG).getAuditLogs(tagId);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForCustomField(final UUID customFieldId) {
+ return getAuditLogs(ObjectType.CUSTOM_FIELD).getAuditLogs(customFieldId);
+ }
+
+ @Override
+ public AccountAuditLogsForObjectType getAuditLogs(final ObjectType objectType) {
+ if (auditLogsCache.get(objectType) == null) {
+ auditLogsCache.put(objectType, new DefaultAccountAuditLogsForObjectType(auditLevel, new ObjectTypeFilter(objectType, accountAuditLogs.iterator())));
+ }
+
+ // Should never be null
+ return auditLogsCache.get(objectType);
+ }
+
+ private final class ObjectTypeFilter extends AbstractIterator<AuditLog> {
+
+ private boolean hasSeenObjectType = false;
+
+ private final ObjectType objectType;
+ private final Iterator<AuditLog> accountAuditLogs;
+
+ private ObjectTypeFilter(final ObjectType objectType, final Iterator<AuditLog> accountAuditLogs) {
+ this.objectType = objectType;
+ this.accountAuditLogs = accountAuditLogs;
+ }
+
+ @Override
+ protected AuditLog computeNext() {
+ while (accountAuditLogs.hasNext()) {
+ final AuditLog element = accountAuditLogs.next();
+ if (predicate.apply(element)) {
+ hasSeenObjectType = true;
+ return element;
+ } else if (hasSeenObjectType) {
+ // Optimization trick: audit log records are ordered first by table name
+ // (hence object type) - when we are done and we switch to another ObjectType,
+ // we are guaranteed there is nothing left to do
+ return endOfData();
+ }
+ }
+
+ return endOfData();
+ }
+
+ private final Predicate<AuditLog> predicate = new Predicate<AuditLog>() {
+ @Override
+ public boolean apply(final AuditLog auditLog) {
+ return objectType.equals(auditLog.getAuditedObjectType());
+ }
+ };
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/audit/DefaultAccountAuditLogsForObjectType.java b/util/src/main/java/org/killbill/billing/util/audit/DefaultAccountAuditLogsForObjectType.java
new file mode 100644
index 0000000..e8441c1
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/audit/DefaultAccountAuditLogsForObjectType.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.util.api.AuditLevel;
+import org.killbill.billing.util.customfield.ShouldntHappenException;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+
+public class DefaultAccountAuditLogsForObjectType implements AccountAuditLogsForObjectType {
+
+ private final Map<UUID, List<AuditLog>> auditLogsCache;
+
+ private final AuditLevel auditLevel;
+ private final Iterator<AuditLog> allAuditLogsForObjectType;
+
+ public DefaultAccountAuditLogsForObjectType(final AuditLevel auditLevel) {
+ this(auditLevel, Iterators.<AuditLog>emptyIterator());
+ }
+
+ public DefaultAccountAuditLogsForObjectType(final AuditLevel auditLevel, final Iterator<AuditLog> allAuditLogsForObjectType) {
+ this.auditLevel = auditLevel;
+ this.auditLogsCache = new HashMap<UUID, List<AuditLog>>();
+ this.allAuditLogsForObjectType = allAuditLogsForObjectType;
+ }
+
+ // Used by DefaultAccountAuditLogs
+ void initializeIfNeeded(final UUID objectId) {
+ if (auditLogsCache.get(objectId) == null) {
+ auditLogsCache.put(objectId, new LinkedList<AuditLog>());
+ }
+ }
+
+ public void close() {
+ // Make sure to go through the results to close the connection
+ while (allAuditLogsForObjectType.hasNext()) {
+ allAuditLogsForObjectType.next();
+ }
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogs(final UUID objectId) {
+ switch (auditLevel) {
+ case FULL:
+ // We need to go through the whole list
+ cacheAllAuditLogs();
+
+ // We went through the whole list, mark we don't have any entry for it if needed
+ initializeIfNeeded(objectId);
+
+ // Should never be null
+ return auditLogsCache.get(objectId);
+ case MINIMAL:
+ if (auditLogsCache.get(objectId) == null) {
+ // We just want the first INSERT audit log
+ final AuditLog candidate = Iterators.<AuditLog>tryFind(allAuditLogsForObjectType,
+ new Predicate<AuditLog>() {
+ @Override
+ public boolean apply(final AuditLog auditLog) {
+ // As we consume the data source, cache the entries
+ cacheAuditLog(auditLog);
+
+ return objectId.equals(auditLog.getAuditedEntityId()) &&
+ // Given our ordering, this should always be true for the first entry
+ ChangeType.INSERT.equals(auditLog.getChangeType());
+ }
+ }).orNull();
+
+ if (candidate == null) {
+ // We went through the whole list, mark we don't have any entry for it
+ initializeIfNeeded(objectId);
+ }
+ }
+
+ // Should never be null
+ return auditLogsCache.get(objectId);
+ case NONE:
+ // Close the connection ASAP since we won't need it
+ close();
+ return ImmutableList.<AuditLog>of();
+ default:
+ throw new ShouldntHappenException("AuditLevel " + auditLevel + " unsupported");
+ }
+ }
+
+ private void cacheAllAuditLogs() {
+ while (allAuditLogsForObjectType.hasNext()) {
+ final AuditLog auditLog = allAuditLogsForObjectType.next();
+ cacheAuditLog(auditLog);
+ }
+ }
+
+ private void cacheAuditLog(final AuditLog auditLog) {
+ initializeIfNeeded(auditLog.getAuditedEntityId());
+ auditLogsCache.get(auditLog.getAuditedEntityId()).add(auditLog);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultAccountAuditLogsForObjectType{");
+ sb.append("auditLogsCache=").append(auditLogsCache);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultAccountAuditLogsForObjectType that = (DefaultAccountAuditLogsForObjectType) o;
+
+ if (!auditLogsCache.equals(that.auditLogsCache)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return auditLogsCache.hashCode();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/audit/DefaultAuditLog.java b/util/src/main/java/org/killbill/billing/util/audit/DefaultAuditLog.java
new file mode 100644
index 0000000..1203035
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/audit/DefaultAuditLog.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.audit.dao.AuditLogModelDao;
+
+public class DefaultAuditLog extends EntityBase implements AuditLog {
+
+ private final AuditLogModelDao auditLogModelDao;
+ private final ObjectType objectType;
+ private final UUID auditedEntityId;
+
+ public DefaultAuditLog(final AuditLogModelDao auditLogModelDao, final ObjectType objectType, final UUID auditedEntityId) {
+ super(auditLogModelDao);
+ this.auditLogModelDao = auditLogModelDao;
+ this.objectType = objectType;
+ this.auditedEntityId = auditedEntityId;
+ }
+
+ @Override
+ public UUID getAuditedEntityId() {
+ return auditedEntityId;
+ }
+
+ @Override
+ public ObjectType getAuditedObjectType() {
+ return objectType;
+ }
+
+ @Override
+ public ChangeType getChangeType() {
+ return auditLogModelDao.getChangeType();
+ }
+
+ @Override
+ public String getUserName() {
+ return auditLogModelDao.getCallContext().getUserName();
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return auditLogModelDao.getCallContext().getCreatedDate();
+ }
+
+ @Override
+ public String getReasonCode() {
+ return auditLogModelDao.getCallContext().getReasonCode();
+ }
+
+ @Override
+ public String getUserToken() {
+ if (auditLogModelDao.getCallContext().getUserToken() == null) {
+ return null;
+ } else {
+ return auditLogModelDao.getCallContext().getUserToken().toString();
+ }
+ }
+
+ @Override
+ public String getComment() {
+ return auditLogModelDao.getCallContext().getComments();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultAuditLog{");
+ sb.append("auditLogModelDao=").append(auditLogModelDao);
+ sb.append(", auditedEntityId=").append(auditedEntityId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final DefaultAuditLog that = (DefaultAuditLog) o;
+
+ if (auditLogModelDao != null ? !auditLogModelDao.equals(that.auditLogModelDao) : that.auditLogModelDao != null) {
+ return false;
+ }
+ if (auditedEntityId != null ? !auditedEntityId.equals(that.auditedEntityId) : that.auditedEntityId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (auditLogModelDao != null ? auditLogModelDao.hashCode() : 0);
+ result = 31 * result + (auditedEntityId != null ? auditedEntityId.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/bus/DefaultBusService.java b/util/src/main/java/org/killbill/billing/util/bus/DefaultBusService.java
new file mode 100644
index 0000000..5fbc361
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/bus/DefaultBusService.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.bus;
+
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.lifecycle.LifecycleHandlerType;
+import org.killbill.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.inject.Inject;
+
+public class DefaultBusService implements BusService {
+
+
+ public static final String EVENT_BUS_GROUP_NAME = "bus-grp";
+ public static final String EVENT_BUS_TH_NAME = "bus-th";
+
+ public static final String EVENT_BUS_SERVICE = "bus-service";
+ public static final String EVENT_BUS_IDENTIFIER = EVENT_BUS_SERVICE;
+
+ private final PersistentBus eventBus;
+
+ @Inject
+ public DefaultBusService(final PersistentBus eventBus) {
+ this.eventBus = eventBus;
+ }
+
+ @Override
+ public String getName() {
+ return EVENT_BUS_SERVICE;
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.INIT_BUS)
+ public void startBus() {
+ eventBus.start();
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_BUS)
+ public void stopBus() {
+ eventBus.stop();
+ }
+
+ @Override
+ public PersistentBus getBus() {
+ return eventBus;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/bus/InMemoryBusModule.java b/util/src/main/java/org/killbill/billing/util/bus/InMemoryBusModule.java
new file mode 100644
index 0000000..76dd5ef
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/bus/InMemoryBusModule.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.bus;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.util.glue.BusModule;
+
+public class InMemoryBusModule extends BusModule {
+
+ public InMemoryBusModule(final ConfigSource configSource) {
+ super(BusType.MEMORY, configSource);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/AccountRecordIdCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/AccountRecordIdCacheLoader.java
new file mode 100644
index 0000000..906f452
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/AccountRecordIdCacheLoader.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.util.UUID;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import net.sf.ehcache.loader.CacheLoader;
+
+@Singleton
+public class AccountRecordIdCacheLoader extends BaseCacheLoader implements CacheLoader {
+
+ @Inject
+ public AccountRecordIdCacheLoader(final IDBI dbi, final NonEntityDao nonEntityDao) {
+ super(dbi, nonEntityDao);
+ }
+
+ @Override
+ public CacheType getCacheType() {
+ return CacheType.ACCOUNT_RECORD_ID;
+ }
+
+ @Override
+ public Object load(final Object key, final Object argument) {
+ checkCacheLoaderStatus();
+
+ if (!(key instanceof String)) {
+ throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
+ }
+ if (!(argument instanceof CacheLoaderArgument)) {
+ throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
+ }
+
+ final String objectId = (String) key;
+ final ObjectType objectType = ((CacheLoaderArgument) argument).getObjectType();
+
+ return nonEntityDao.retrieveAccountRecordIdFromObject(UUID.fromString(objectId), objectType, null);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/AuditLogCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/AuditLogCacheLoader.java
new file mode 100644
index 0000000..329bba5
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/AuditLogCacheLoader.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.dao.AuditSqlDao;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import net.sf.ehcache.loader.CacheLoader;
+
+@Singleton
+public class AuditLogCacheLoader extends BaseCacheLoader implements CacheLoader {
+
+ private final AuditSqlDao auditSqlDao;
+
+ @Inject
+ public AuditLogCacheLoader(final IDBI dbi, final NonEntityDao nonEntityDao) {
+ super(dbi, nonEntityDao);
+ this.auditSqlDao = dbi.onDemand(AuditSqlDao.class);
+ }
+
+ @Override
+ public CacheType getCacheType() {
+ return CacheType.AUDIT_LOG;
+ }
+
+ @Override
+ public Object load(final Object key, final Object argument) {
+ checkCacheLoaderStatus();
+
+ if (!(key instanceof String)) {
+ throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
+ }
+ if (!(argument instanceof CacheLoaderArgument)) {
+ throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
+ }
+
+ final Object[] args = ((CacheLoaderArgument) argument).getArgs();
+ final String tableName = (String) args[0];
+ final Long targetRecordId = (Long) args[1];
+ final InternalTenantContext internalTenantContext = (InternalTenantContext) args[2];
+
+ return auditSqlDao.getAuditLogsForTargetRecordId(tableName, targetRecordId, internalTenantContext);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/AuditLogViaHistoryCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/AuditLogViaHistoryCacheLoader.java
new file mode 100644
index 0000000..8b1e76c
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/AuditLogViaHistoryCacheLoader.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.dao.AuditSqlDao;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import net.sf.ehcache.loader.CacheLoader;
+
+@Singleton
+public class AuditLogViaHistoryCacheLoader extends BaseCacheLoader implements CacheLoader {
+
+ private final AuditSqlDao auditSqlDao;
+
+ @Inject
+ public AuditLogViaHistoryCacheLoader(final IDBI dbi, final NonEntityDao nonEntityDao) {
+ super(dbi, nonEntityDao);
+ this.auditSqlDao = dbi.onDemand(AuditSqlDao.class);
+ }
+
+ @Override
+ public CacheType getCacheType() {
+ return CacheType.AUDIT_LOG_VIA_HISTORY;
+ }
+
+ @Override
+ public Object load(final Object key, final Object argument) {
+ checkCacheLoaderStatus();
+
+ if (!(key instanceof String)) {
+ throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
+ }
+ if (!(argument instanceof CacheLoaderArgument)) {
+ throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
+ }
+
+ final Object[] args = ((CacheLoaderArgument) argument).getArgs();
+ final String tableName = (String) args[0];
+ final String historyTableName = (String) args[1];
+ final Long targetRecordId = (Long) args[2];
+ final InternalTenantContext internalTenantContext = (InternalTenantContext) args[3];
+
+ return auditSqlDao.getAuditLogsViaHistoryForTargetRecordId(tableName, historyTableName, targetRecordId, internalTenantContext);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/BaseCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/BaseCacheLoader.java
new file mode 100644
index 0000000..782cf0a
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/BaseCacheLoader.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.util.Collection;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import net.sf.ehcache.CacheException;
+import net.sf.ehcache.Ehcache;
+import net.sf.ehcache.Status;
+import net.sf.ehcache.loader.CacheLoader;
+
+public abstract class BaseCacheLoader implements CacheLoader {
+
+ protected final IDBI dbi;
+ protected final NonEntityDao nonEntityDao;
+
+ private Status cacheLoaderStatus;
+
+ @Inject
+ public BaseCacheLoader(final IDBI dbi, final NonEntityDao nonEntityDao) {
+ this.dbi = dbi;
+ this.nonEntityDao = nonEntityDao;
+ this.cacheLoaderStatus = Status.STATUS_UNINITIALISED;
+ }
+
+ public abstract CacheType getCacheType();
+
+ @Override
+ public abstract Object load(final Object key, final Object argument);
+
+ @Override
+ public Object load(final Object key) throws CacheException {
+ throw new IllegalStateException("Method load is not implemented ");
+ }
+
+ @Override
+ public Map loadAll(final Collection keys) {
+ throw new IllegalStateException("Method loadAll is not implemented ");
+ }
+
+ @Override
+ public Map loadAll(final Collection keys, final Object argument) {
+ throw new IllegalStateException("Method loadAll is not implemented ");
+ }
+
+ @Override
+ public String getName() {
+ return this.getClass().getName();
+ }
+
+ @Override
+ public CacheLoader clone(final Ehcache cache) throws CloneNotSupportedException {
+ throw new IllegalStateException("Method clone is not implemented ");
+ }
+
+ @Override
+ public void init() {
+ this.cacheLoaderStatus = Status.STATUS_ALIVE;
+ }
+
+ @Override
+ public void dispose() throws CacheException {
+ cacheLoaderStatus = Status.STATUS_SHUTDOWN;
+ }
+
+ @Override
+ public Status getStatus() {
+ return cacheLoaderStatus;
+ }
+
+ protected void checkCacheLoaderStatus() {
+ if (getStatus() != Status.STATUS_ALIVE) {
+ throw new CacheException("CacheLoader is not available!");
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/Cachable.java b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
new file mode 100644
index 0000000..d9ffbe2
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface Cachable {
+
+ public final String RECORD_ID_CACHE_NAME = "record-id";
+ public final String ACCOUNT_RECORD_ID_CACHE_NAME = "account-record-id";
+ public final String TENANT_RECORD_ID_CACHE_NAME = "tenant-record-id";
+ public final String AUDIT_LOG_CACHE_NAME = "audit-log";
+ public final String AUDIT_LOG_VIA_HISTORY_CACHE_NAME = "audit-log-via-history";
+
+ public CacheType value();
+
+ public enum CacheType {
+ /* Mapping from object 'id (UUID)' -> object 'recordId (Long' */
+ RECORD_ID(RECORD_ID_CACHE_NAME),
+
+ /* Mapping from object 'id (UUID)' -> matching account object 'accountRecordId (Long)' */
+ ACCOUNT_RECORD_ID(ACCOUNT_RECORD_ID_CACHE_NAME),
+
+ /* Mapping from object 'id (UUID)' -> matching object 'tenantRecordId (Long)' */
+ TENANT_RECORD_ID(TENANT_RECORD_ID_CACHE_NAME),
+
+ /* Mapping from object 'tableName::targetRecordId' -> matching objects 'Iterable<AuditLog>' */
+ AUDIT_LOG(AUDIT_LOG_CACHE_NAME),
+
+ /* Mapping from object 'tableName::historyTableName::targetRecordId' -> matching objects 'Iterable<AuditLog>' */
+ AUDIT_LOG_VIA_HISTORY(AUDIT_LOG_VIA_HISTORY_CACHE_NAME);
+
+ private final String cacheName;
+
+ CacheType(final String cacheName) {
+ this.cacheName = cacheName;
+ }
+
+ public String getCacheName() {
+ return cacheName;
+ }
+
+ public static CacheType findByName(final String input) {
+ for (final CacheType cacheType : CacheType.values()) {
+ if (cacheType.cacheName.equals(input)) {
+ return cacheType;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CachableKey.java b/util/src/main/java/org/killbill/billing/util/cache/CachableKey.java
new file mode 100644
index 0000000..69cf057
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/CachableKey.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface CachableKey {
+
+ // Position (start at 1)
+ public int value();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheController.java b/util/src/main/java/org/killbill/billing/util/cache/CacheController.java
new file mode 100644
index 0000000..1b357ea
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/CacheController.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+public interface CacheController<K, V> {
+
+ public V get(K key, CacheLoaderArgument objectType);
+
+ public boolean remove(K key);
+
+ public int size();
+
+ void removeAll();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
new file mode 100644
index 0000000..11c44e3
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.util.cache.Cachable.CacheType;
+
+// Kill Bill generic cache dispatcher
+public class CacheControllerDispatcher {
+
+ private final Map<CacheType, CacheController<Object, Object>> caches;
+
+ @Inject
+ public CacheControllerDispatcher(final Map<CacheType, CacheController<Object, Object>> caches) {
+ this.caches = caches;
+ }
+
+ // Test only
+ public CacheControllerDispatcher() {
+ caches = new HashMap<CacheType, CacheController<Object, Object>>();
+ }
+
+ public CacheController<Object, Object> getCacheController(final CacheType cacheType) {
+ return caches.get(cacheType);
+ }
+
+ public void clearAll() {
+ for (final CacheController<Object, Object> cacheController : caches.values()) {
+ cacheController.removeAll();
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcherProvider.java b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcherProvider.java
new file mode 100644
index 0000000..e216033
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcherProvider.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.killbill.billing.util.cache.Cachable.CacheType;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.loader.CacheLoader;
+
+// Build the abstraction layer between EhCache and Kill Bill
+public class CacheControllerDispatcherProvider implements Provider<CacheControllerDispatcher> {
+
+ private final CacheManager cacheManager;
+
+ @Inject
+ public CacheControllerDispatcherProvider(final CacheManager cacheManager) {
+ this.cacheManager = cacheManager;
+ }
+
+ @Override
+ public CacheControllerDispatcher get() {
+ final Map<CacheType, CacheController<Object, Object>> cacheControllers = new LinkedHashMap<CacheType, CacheController<Object, Object>>();
+ for (final String cacheName : cacheManager.getCacheNames()) {
+ final CacheType cacheType = CacheType.findByName(cacheName);
+
+ final Collection<EhCacheBasedCacheController<Object, Object>> cacheControllersForCacheName = getCacheControllersForCacheName(cacheName);
+ // EhCache supports multiple cache loaders per type, but not Kill Bill - take the first one
+ if (cacheControllersForCacheName.size() > 0) {
+ final EhCacheBasedCacheController<Object, Object> ehCacheBasedCacheController = cacheControllersForCacheName.iterator().next();
+ cacheControllers.put(cacheType, ehCacheBasedCacheController);
+ }
+ }
+
+ return new CacheControllerDispatcher(cacheControllers);
+ }
+
+ public Collection<EhCacheBasedCacheController<Object, Object>> getCacheControllersForCacheName(final String name) {
+ final Cache cache = cacheManager.getCache(name);
+
+ // The CacheLoaders were registered in EhCacheCacheManagerProvider
+ return Collections2.transform(cache.getRegisteredCacheLoaders(), new Function<CacheLoader, EhCacheBasedCacheController<Object, Object>>() {
+ @Override
+ public EhCacheBasedCacheController<Object, Object> apply(final CacheLoader input) {
+ return new EhCacheBasedCacheController<Object, Object>(cache);
+ }
+ });
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheLoaderArgument.java b/util/src/main/java/org/killbill/billing/util/cache/CacheLoaderArgument.java
new file mode 100644
index 0000000..7035fab
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/CacheLoaderArgument.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+public class CacheLoaderArgument {
+
+ private final ObjectType objectType;
+ private final Object[] args;
+ private final InternalTenantContext internalTenantContext;
+
+ public CacheLoaderArgument(final ObjectType objectType) {
+ this(objectType, new Object[]{}, null);
+ }
+
+ public CacheLoaderArgument(final ObjectType objectType, final Object[] args, @Nullable final InternalTenantContext internalTenantContext) {
+ this.objectType = objectType;
+ this.args = args;
+ this.internalTenantContext = internalTenantContext;
+ }
+
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ public Object[] getArgs() {
+ return args;
+ }
+
+ public InternalTenantContext getInternalTenantContext() {
+ return internalTenantContext;
+ }
+}
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
new file mode 100644
index 0000000..d12b5d6
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.Element;
+
+public class EhCacheBasedCacheController<K, V> implements CacheController<K, V> {
+
+ private final Cache cache;
+
+ public EhCacheBasedCacheController(final Cache cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public V get(final K key, final CacheLoaderArgument cacheLoaderArgument) {
+ final Element element = cache.getWithLoader(key, null, cacheLoaderArgument);
+ if (element == null) {
+ return null;
+ }
+ return (V) element.getObjectValue();
+ }
+
+ @Override
+ public boolean remove(final K key) {
+ return cache.remove(key);
+ }
+
+ @Override
+ public int size() {
+ return cache.getSize();
+ }
+
+ @Override
+ public void removeAll() {
+ cache.removeAll();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java b/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
new file mode 100644
index 0000000..a4f50f9
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.LinkedList;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.killbill.billing.util.config.CacheConfig;
+
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.loader.CacheLoader;
+
+// EhCache specific provider
+public class EhCacheCacheManagerProvider implements Provider<CacheManager> {
+
+ private final CacheConfig cacheConfig;
+ private final Collection<BaseCacheLoader> cacheLoaders = new LinkedList<BaseCacheLoader>();
+
+ @Inject
+ public EhCacheCacheManagerProvider(final CacheConfig cacheConfig,
+ final RecordIdCacheLoader recordIdCacheLoader,
+ final AccountRecordIdCacheLoader accountRecordIdCacheLoader,
+ final TenantRecordIdCacheLoader tenantRecordIdCacheLoader,
+ final AuditLogCacheLoader auditLogCacheLoader,
+ final AuditLogViaHistoryCacheLoader auditLogViaHistoryCacheLoader) {
+ this.cacheConfig = cacheConfig;
+ cacheLoaders.add(recordIdCacheLoader);
+ cacheLoaders.add(accountRecordIdCacheLoader);
+ cacheLoaders.add(tenantRecordIdCacheLoader);
+ cacheLoaders.add(auditLogCacheLoader);
+ cacheLoaders.add(auditLogViaHistoryCacheLoader);
+ }
+
+ @Override
+ public CacheManager get() {
+ final CacheManager cacheManager;
+ try {
+ cacheManager = CacheManager.create(EhCacheCacheManagerProvider.class.getResource(cacheConfig.getCacheConfigLocation()).openStream());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ for (final BaseCacheLoader cacheLoader : cacheLoaders) {
+ cacheLoader.init();
+
+ final Cache cache = cacheManager.getCache(cacheLoader.getCacheType().getCacheName());
+
+ // Make sure we start from a clean state - this is mainly useful for tests
+ for (final CacheLoader existingCacheLoader : cache.getRegisteredCacheLoaders()) {
+ cache.unregisterCacheLoader(existingCacheLoader);
+ }
+
+ cache.registerCacheLoader(cacheLoader);
+ }
+
+ return cacheManager;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/ExpirationListenerFactory.java b/util/src/main/java/org/killbill/billing/util/cache/ExpirationListenerFactory.java
new file mode 100644
index 0000000..3f2145d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/ExpirationListenerFactory.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.util.Properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.sf.ehcache.CacheException;
+import net.sf.ehcache.Ehcache;
+import net.sf.ehcache.Element;
+import net.sf.ehcache.event.CacheEventListener;
+import net.sf.ehcache.event.CacheEventListenerFactory;
+
+public class ExpirationListenerFactory extends CacheEventListenerFactory {
+
+ private static final Logger log = LoggerFactory.getLogger(ExpirationListenerFactory.class);
+
+ @Override
+ public CacheEventListener createCacheEventListener(final Properties properties)
+ {
+ return new ExpirationListener();
+ }
+
+ private static class ExpirationListener implements CacheEventListener
+ {
+ @Override
+ public Object clone() throws CloneNotSupportedException
+ {
+ throw new CloneNotSupportedException("No cloning!");
+ }
+
+ @Override
+ public void dispose()
+ {
+ }
+
+ @Override
+ public void notifyElementEvicted(final Ehcache cache, final Element element)
+ {
+ if (log.isDebugEnabled()) {
+ log.debug("Cache Element " + element + " evicted!");
+ }
+ }
+
+ @Override
+ public void notifyElementExpired(final Ehcache cache, final Element element)
+ {
+ if (log.isDebugEnabled()) {
+ log.debug("Cache Element " + element + " expired!");
+ }
+ }
+
+ @Override
+ public void notifyElementPut(final Ehcache cache, final Element element) throws CacheException
+ {
+ }
+
+ @Override
+ public void notifyElementRemoved(final Ehcache cache, final Element element) throws CacheException
+ {
+ }
+
+ @Override
+ public void notifyElementUpdated(final Ehcache cache, final Element element) throws CacheException
+ {
+ }
+
+ @Override
+ public void notifyRemoveAll(final Ehcache cache)
+ {
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/RecordIdCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/RecordIdCacheLoader.java
new file mode 100644
index 0000000..a2fb825
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/RecordIdCacheLoader.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.util.UUID;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import net.sf.ehcache.CacheException;
+import net.sf.ehcache.loader.CacheLoader;
+
+@Singleton
+public class RecordIdCacheLoader extends BaseCacheLoader implements CacheLoader {
+
+ @Inject
+ public RecordIdCacheLoader(final IDBI dbi, final NonEntityDao nonEntityDao) {
+ super(dbi, nonEntityDao);
+ }
+
+ @Override
+ public CacheType getCacheType() {
+ return CacheType.RECORD_ID;
+ }
+
+ @Override
+ public Object load(final Object key, final Object argument) throws CacheException {
+ checkCacheLoaderStatus();
+
+ if (!(key instanceof String)) {
+ throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
+ }
+ if (!(argument instanceof CacheLoaderArgument)) {
+ throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
+ }
+
+ final String objectId = (String) key;
+ final ObjectType objectType = ((CacheLoaderArgument) argument).getObjectType();
+
+ return nonEntityDao.retrieveRecordIdFromObject(UUID.fromString(objectId), objectType, null);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantRecordIdCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantRecordIdCacheLoader.java
new file mode 100644
index 0000000..753a7fb
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantRecordIdCacheLoader.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.util.UUID;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import net.sf.ehcache.loader.CacheLoader;
+
+@Singleton
+public class TenantRecordIdCacheLoader extends BaseCacheLoader implements CacheLoader {
+
+ @Inject
+ public TenantRecordIdCacheLoader(final IDBI dbi, final NonEntityDao nonEntityDao) {
+ super(dbi, nonEntityDao);
+ }
+
+ @Override
+ public CacheType getCacheType() {
+ return CacheType.TENANT_RECORD_ID;
+ }
+
+ @Override
+ public Object load(final Object key, final Object argument) {
+ checkCacheLoaderStatus();
+
+ if (!(key instanceof String)) {
+ throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
+ }
+ if (!(argument instanceof CacheLoaderArgument)) {
+ throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
+ }
+
+ final String objectId = (String) key;
+ final ObjectType objectType = ((CacheLoaderArgument) argument).getObjectType();
+
+ return nonEntityDao.retrieveTenantRecordIdFromObject(UUID.fromString(objectId), objectType, null);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/callcontext/CallContextFactory.java b/util/src/main/java/org/killbill/billing/util/callcontext/CallContextFactory.java
new file mode 100644
index 0000000..324a610
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/callcontext/CallContextFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.callcontext;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+public interface CallContextFactory {
+
+ TenantContext createTenantContext(@Nullable UUID tenantId);
+
+ CallContext createCallContext(@Nullable UUID tenantId, String userName, CallOrigin callOrigin, UserType userType, UUID userToken);
+
+ CallContext createCallContext(@Nullable UUID tenantId, String userName, CallOrigin callOrigin, UserType userType,
+ String reasonCode, String comment, UUID userToken);
+
+ CallContext createCallContext(@Nullable UUID tenantId, String userName, CallOrigin callOrigin, UserType userType);
+
+ CallContext toMigrationCallContext(@Nullable CallContext callContext, DateTime createdDate, DateTime updatedDate);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/callcontext/DefaultCallContextFactory.java b/util/src/main/java/org/killbill/billing/util/callcontext/DefaultCallContextFactory.java
new file mode 100644
index 0000000..a8f9b22
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/callcontext/DefaultCallContextFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.callcontext;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.callcontext.DefaultCallContext;
+import org.killbill.billing.callcontext.DefaultTenantContext;
+import org.killbill.clock.Clock;
+
+import com.google.inject.Inject;
+
+public class DefaultCallContextFactory implements CallContextFactory {
+
+ private final Clock clock;
+
+ @Inject
+ public DefaultCallContextFactory(final Clock clock) {
+ this.clock = clock;
+ }
+
+ @Override
+ public TenantContext createTenantContext(final UUID tenantId) {
+ return new DefaultTenantContext(tenantId);
+ }
+
+ @Override
+ public CallContext createCallContext(@Nullable final UUID tenantId, final String userName, final CallOrigin callOrigin,
+ final UserType userType, @Nullable final UUID userToken) {
+ return new DefaultCallContext(tenantId, userName, callOrigin, userType, userToken, clock);
+ }
+
+ @Override
+ public CallContext createCallContext(@Nullable final UUID tenantId, final String userName, final CallOrigin callOrigin,
+ final UserType userType, final String reasonCode, final String comment, final UUID userToken) {
+ return new DefaultCallContext(tenantId, userName, callOrigin, userType, reasonCode, comment, userToken, clock);
+ }
+
+ @Override
+ public CallContext createCallContext(@Nullable final UUID tenantId, final String userName, final CallOrigin callOrigin, final UserType userType) {
+ return createCallContext(tenantId, userName, callOrigin, userType, null);
+ }
+
+ @Override
+ public CallContext toMigrationCallContext(final CallContext callContext, final DateTime createdDate, final DateTime updatedDate) {
+ return new MigrationCallContext(callContext, createdDate, updatedDate);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java b/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java
new file mode 100644
index 0000000..6aa8259
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.callcontext;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.clock.Clock;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import com.google.common.base.Objects;
+
+// Internal contexts almost always expect accountRecordId and tenantRecordId to be populated
+public class InternalCallContextFactory {
+
+ public static final long INTERNAL_TENANT_RECORD_ID = 0L;
+
+ private final Clock clock;
+ private final NonEntityDao nonEntityDao;
+ private final CacheControllerDispatcher cacheControllerDispatcher;
+
+ @Inject
+ public InternalCallContextFactory(final Clock clock, final NonEntityDao nonEntityDao, final CacheControllerDispatcher cacheControllerDispatcher) {
+ this.clock = clock;
+ this.nonEntityDao = nonEntityDao;
+ this.cacheControllerDispatcher = cacheControllerDispatcher;
+ }
+
+ /**
+ * Create an internal tenant callcontext from a tenant callcontext
+ * <p/>
+ * This is used for r/o operations - we don't need the account id in that case.
+ * You should almost never use that one, you always want to populate the accountRecordId
+ *
+ * @param context tenant callcontext (tenantId can be null only if multi-tenancy is disabled)
+ * @return internal tenant callcontext
+ */
+ public InternalTenantContext createInternalTenantContext(final TenantContext context) {
+ // If tenant id is null, this will default to the default tenant record id (multi-tenancy disabled)
+ final Long tenantRecordId = getTenantRecordId(context);
+ return createInternalTenantContext(tenantRecordId, null);
+ }
+
+ /**
+ * Create an internal tenant callcontext
+ *
+ * @param tenantRecordId tenant_record_id (cannot be null)
+ * @param accountRecordId account_record_id (cannot be null for INSERT operations)
+ * @return internal tenant callcontext
+ */
+ public InternalTenantContext createInternalTenantContext(final Long tenantRecordId, @Nullable final Long accountRecordId) {
+ //Preconditions.checkNotNull(tenantRecordId, "tenantRecordId cannot be null");
+ return new InternalTenantContext(tenantRecordId, accountRecordId);
+ }
+
+ public InternalTenantContext createInternalTenantContext(final UUID accountId, final TenantContext context) {
+ final Long tenantRecordId = getTenantRecordId(context);
+ final Long accountRecordId = getAccountRecordId(accountId, ObjectType.ACCOUNT);
+ return new InternalTenantContext(tenantRecordId, accountRecordId);
+ }
+
+ public InternalTenantContext createInternalTenantContext(final UUID accountId, final InternalTenantContext context) {
+ final Long tenantRecordId = context.getTenantRecordId();
+ final Long accountRecordId = getAccountRecordId(accountId, ObjectType.ACCOUNT);
+ return new InternalTenantContext(tenantRecordId, accountRecordId);
+ }
+
+ /**
+ * Crate an internal tenant callcontext from a tenant callcontext, and retrieving the account_record_id from another table
+ *
+ * @param objectId the id of the row in the table pointed by object type where to look for account_record_id
+ * @param objectType the object type pointed by this objectId
+ * @param context original tenant callcontext
+ * @return internal tenant callcontext from callcontext, with a non null account_record_id (if found)
+ */
+ public InternalTenantContext createInternalTenantContext(final UUID objectId, final ObjectType objectType, final TenantContext context) {
+ // The callcontext may come from a user API - for security, check we're not doing cross-tenants operations
+ //final Long tenantRecordIdFromObject = retrieveTenantRecordIdFromObject(objectId, objectType);
+ //final Long tenantRecordIdFromContext = getTenantRecordId(callcontext);
+ //Preconditions.checkState(tenantRecordIdFromContext.equals(tenantRecordIdFromObject),
+ // "tenant of the pointed object (%s) and the callcontext (%s) don't match!", tenantRecordIdFromObject, tenantRecordIdFromContext);
+ final Long tenantRecordId = getTenantRecordId(context);
+ final Long accountRecordId = getAccountRecordId(objectId, objectType);
+ return createInternalTenantContext(tenantRecordId, accountRecordId);
+ }
+
+ /**
+ * Create an internal call callcontext from a call callcontext, and retrieving the account_record_id from another table
+ *
+ * @param objectId the id of the row in the table pointed by object type where to look for account_record_id
+ * @param objectType the object type pointed by this objectId
+ * @param context original call callcontext
+ * @return internal call callcontext from callcontext, with a non null account_record_id (if found)
+ */
+ public InternalCallContext createInternalCallContext(final UUID objectId, final ObjectType objectType, final CallContext context) {
+ // The callcontext may come from a user API - for security, check we're not doing cross-tenants operations
+ //final Long tenantRecordIdFromObject = retrieveTenantRecordIdFromObject(objectId, objectType);
+ //final Long tenantRecordIdFromContext = getTenantRecordId(callcontext);
+ //Preconditions.checkState(tenantRecordIdFromContext.equals(tenantRecordIdFromObject),
+ // "tenant of the pointed object (%s) and the callcontext (%s) don't match!", tenantRecordIdFromObject, tenantRecordIdFromContext);
+
+ return createInternalCallContext(objectId, objectType, context.getUserName(), context.getCallOrigin(),
+ context.getUserType(), context.getUserToken(), context.getReasonCode(), context.getComments(),
+ context.getCreatedDate(), context.getUpdatedDate());
+ }
+
+ /**
+ * Create an internal call callcontext using an existing account to retrieve tenant and account record ids
+ * <p/>
+ * This is used for r/w operations - we need the account id to populate the account_record_id field
+ *
+ * @param accountId account id
+ * @param context original call callcontext
+ * @return internal call callcontext
+ */
+ public InternalCallContext createInternalCallContext(final UUID accountId, final CallContext context) {
+ return createInternalCallContext(accountId, ObjectType.ACCOUNT, context.getUserName(), context.getCallOrigin(),
+ context.getUserType(), context.getUserToken(), context.getReasonCode(), context.getComments(),
+ context.getCreatedDate(), context.getUpdatedDate());
+ }
+
+ /**
+ * Create an internal call callcontext using an existing account to retrieve tenant and account record ids
+ *
+ * @param accountId account id
+ * @param userName user name
+ * @param callOrigin call origin
+ * @param userType user type
+ * @param userToken user token, if any
+ * @return internal call callcontext
+ */
+ public InternalCallContext createInternalCallContext(final UUID accountId, final String userName, final CallOrigin callOrigin,
+ final UserType userType, @Nullable final UUID userToken) {
+ return createInternalCallContext(accountId, ObjectType.ACCOUNT, userName, callOrigin, userType, userToken);
+ }
+
+ /**
+ * Create an internal call callcontext using an existing object to retrieve tenant and account record ids
+ *
+ * @param objectId the id of the row in the table pointed by object type where to look for account_record_id
+ * @param objectType the object type pointed by this objectId
+ * @param userName user name
+ * @param callOrigin call origin
+ * @param userType user type
+ * @param userToken user token, if any
+ * @return internal call callcontext
+ */
+ public InternalCallContext createInternalCallContext(final UUID objectId, final ObjectType objectType, final String userName,
+ final CallOrigin callOrigin, final UserType userType, @Nullable final UUID userToken) {
+ return createInternalCallContext(objectId, objectType, userName, callOrigin, userType, userToken, null, null, clock.getUTCNow(), clock.getUTCNow());
+ }
+
+ public InternalCallContext createInternalCallContext(final UUID objectId, final ObjectType objectType, final String userName,
+ final CallOrigin callOrigin, final UserType userType, @Nullable final UUID userToken,
+ @Nullable final String reasonCode, @Nullable final String comment, final DateTime createdDate,
+ final DateTime updatedDate) {
+ final Long tenantRecordId = nonEntityDao.retrieveTenantRecordIdFromObject(objectId, objectType, cacheControllerDispatcher.getCacheController(CacheType.TENANT_RECORD_ID));
+ final Long accountRecordId = getAccountRecordId(objectId, objectType);
+ return createInternalCallContext(tenantRecordId, accountRecordId, userName, callOrigin, userType, userToken,
+ reasonCode, comment, createdDate, updatedDate);
+ }
+
+ /**
+ * Create an internal call callcontext
+ * <p/>
+ * This is used by notification queue and persistent bus - accountRecordId is expected to be non null
+ *
+ * @param tenantRecordId tenant record id - if null, the default tenant record id value will be used
+ * @param accountRecordId account record id (cannot be null)
+ * @param userName user name
+ * @param callOrigin call origin
+ * @param userType user type
+ * @param userToken user token, if any
+ * @return internal call callcontext
+ */
+ public InternalCallContext createInternalCallContext(@Nullable final Long tenantRecordId, final Long accountRecordId, final String userName,
+ final CallOrigin callOrigin, final UserType userType, @Nullable final UUID userToken) {
+ return new InternalCallContext(tenantRecordId, accountRecordId, userToken, userName, callOrigin, userType, null, null,
+ clock.getUTCNow(), clock.getUTCNow());
+ }
+
+ private InternalCallContext createInternalCallContext(@Nullable final Long tenantRecordId, final Long accountRecordId, final String userName,
+ final CallOrigin callOrigin, final UserType userType, @Nullable final UUID userToken,
+ @Nullable final String reasonCode, @Nullable final String comment, final DateTime createdDate,
+ final DateTime updatedDate) {
+ //Preconditions.checkNotNull(accountRecordId, "accountRecordId cannot be null");
+ final Long nonNulTenantRecordId = Objects.firstNonNull(tenantRecordId, INTERNAL_TENANT_RECORD_ID);
+
+ return new InternalCallContext(nonNulTenantRecordId, accountRecordId, userToken, userName, callOrigin, userType, reasonCode, comment,
+ createdDate, updatedDate);
+ }
+
+ /**
+ * Create an internal call callcontext without populating the account record id
+ * <p/>
+ * This is used for update/delete operations - we don't need the account id in that case - and
+ * also when we don't have an account_record_id column (e.g. tenants, tag_definitions)
+ *
+ * @param context original call callcontext
+ * @return internal call callcontext
+ */
+ public InternalCallContext createInternalCallContext(final CallContext context) {
+ // If tenant id is null, this will default to the default tenant record id (multi-tenancy disabled)
+ final Long tenantRecordId = getTenantRecordId(context);
+ return new InternalCallContext(tenantRecordId, null, context);
+ }
+
+ // Used when we need to re-hydrate the callcontext with the account_record_id (when creating the account)
+ public InternalCallContext createInternalCallContext(final Long accountRecordId, final InternalCallContext context) {
+ return new InternalCallContext(context.getTenantRecordId(), accountRecordId, context.getUserToken(), context.getCreatedBy(),
+ context.getCallOrigin(), context.getContextUserType(), context.getReasonCode(), context.getComments(),
+ context.getCreatedDate(), context.getUpdatedDate());
+ }
+
+ // Used when we need to re-hydrate the callcontext with the tenant_record_id and account_record_id (when claiming bus events)
+ public InternalCallContext createInternalCallContext(final Long tenantRecordId, final Long accountRecordId, final InternalCallContext context) {
+ return new InternalCallContext(tenantRecordId, accountRecordId, context.getUserToken(), context.getCreatedBy(),
+ context.getCallOrigin(), context.getContextUserType(), context.getReasonCode(), context.getComments(),
+ context.getCreatedDate(), context.getUpdatedDate());
+ }
+
+ private Long getAccountRecordId(final UUID accountId) {
+ return getAccountRecordId(accountId, ObjectType.ACCOUNT);
+ }
+
+ private Long getAccountRecordId(final UUID objectId, final ObjectType objectType) {
+ return nonEntityDao.retrieveAccountRecordIdFromObject(objectId, objectType, cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_RECORD_ID));
+ }
+
+ private Long getTenantRecordId(final TenantContext context) {
+ // Default to single default tenant (e.g. single tenant mode)
+ // TODO Extract this convention (e.g. BusinessAnalyticsBase needs to know about it)
+ if (context.getTenantId() == null) {
+ return INTERNAL_TENANT_RECORD_ID;
+ } else {
+ return nonEntityDao.retrieveTenantRecordIdFromObject(context.getTenantId(), ObjectType.TENANT, cacheControllerDispatcher.getCacheController(CacheType.TENANT_RECORD_ID));
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/callcontext/InternalTenantContextBinder.java b/util/src/main/java/org/killbill/billing/util/callcontext/InternalTenantContextBinder.java
new file mode 100644
index 0000000..038d28c
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/callcontext/InternalTenantContextBinder.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.callcontext;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.sql.Types;
+
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.callcontext.InternalTenantContextBinder.InternalTenantContextBinderFactory;
+
+@BindingAnnotation(InternalTenantContextBinderFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface InternalTenantContextBinder {
+
+ public static class InternalTenantContextBinderFactory implements BinderFactory {
+
+ @Override
+ public Binder build(final Annotation annotation) {
+ return new Binder<InternalTenantContextBinder, InternalTenantContext>() {
+ @Override
+ public void bind(final SQLStatement q, final InternalTenantContextBinder bind, final InternalTenantContext context) {
+ if (context.getTenantRecordId() == null) {
+ // TODO - shouldn't be null, but for now...
+ q.bindNull("tenantRecordId", Types.INTEGER);
+ } else {
+ q.bind("tenantRecordId", context.getTenantRecordId());
+ }
+
+ if (context.getAccountRecordId() == null) {
+ q.bindNull("accountRecordId", Types.INTEGER);
+ } else {
+ q.bind("accountRecordId", context.getAccountRecordId());
+ }
+
+ if (context instanceof InternalCallContext) {
+ final InternalCallContext callContext = (InternalCallContext) context;
+ q.bind("userName", callContext.getCreatedBy());
+ if (callContext.getCreatedDate() == null) {
+ q.bindNull("createdDate", Types.DATE);
+ } else {
+ q.bind("createdDate", callContext.getCreatedDate().toDate());
+ }
+ if (callContext.getUpdatedDate() == null) {
+ q.bindNull("updatedDate", Types.DATE);
+ } else {
+ q.bind("updatedDate", callContext.getUpdatedDate().toDate());
+ }
+ q.bind("reasonCode", callContext.getReasonCode());
+ q.bind("comments", callContext.getComments());
+ q.bind("userToken", (callContext.getUserToken() != null) ? callContext.getUserToken().toString() : null);
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/callcontext/MigrationCallContext.java b/util/src/main/java/org/killbill/billing/util/callcontext/MigrationCallContext.java
new file mode 100644
index 0000000..3cbb007
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/callcontext/MigrationCallContext.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.callcontext;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.callcontext.CallContextBase;
+
+public class MigrationCallContext extends CallContextBase {
+
+ private final DateTime createdDate;
+ private final DateTime updatedDate;
+
+ public MigrationCallContext(final CallContext context, final DateTime createdDate, final DateTime updatedDate) {
+ super(context.getTenantId(), context.getUserName(), context.getCallOrigin(), context.getUserType());
+ this.createdDate = createdDate;
+ this.updatedDate = updatedDate;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return createdDate;
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return updatedDate;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/CacheConfig.java b/util/src/main/java/org/killbill/billing/util/config/CacheConfig.java
new file mode 100644
index 0000000..b3ee5a9
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/CacheConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+public interface CacheConfig extends KillbillConfig {
+
+ @Config("org.killbill.cache.config.location")
+ @Default("/ehcache.xml")
+ @Description("Path to Ehcache XML configuration")
+ public String getCacheConfigLocation();
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/catalog/UriAccessor.java b/util/src/main/java/org/killbill/billing/util/config/catalog/UriAccessor.java
new file mode 100644
index 0000000..b3aad01
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/catalog/UriAccessor.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config.catalog;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Scanner;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.io.Resources;
+
+public class UriAccessor {
+
+ private final static Logger log = LoggerFactory.getLogger(UriAccessor.class);
+
+ private static final String URI_SCHEME_FOR_CLASSPATH = "jar";
+ private static final String URI_SCHEME_FOR_FILE = "file";
+
+ public static InputStream accessUri(String uri) throws IOException, URISyntaxException {
+ return accessUri(new URI(uri));
+ }
+
+ public static InputStream accessUri(URI uri) throws IOException, URISyntaxException {
+ String scheme = uri.getScheme();
+ URL url = null;
+ if (scheme == null) {
+ uri = new URI(Resources.getResource(uri.toString()).toExternalForm());
+ } else if (scheme.equals(URI_SCHEME_FOR_CLASSPATH)) {
+ return UriAccessor.class.getResourceAsStream(uri.getPath());
+ } else if (scheme.equals(URI_SCHEME_FOR_FILE) &&
+ !uri.getSchemeSpecificPart().startsWith("/")) { // interpret URIs of this form as relative path uris
+ url = new File(uri.getSchemeSpecificPart()).toURI().toURL();
+ }
+ url = uri.toURL();
+ return url.openConnection().getInputStream();
+ }
+
+ public static String accessUriAsString(String uri) throws IOException, URISyntaxException {
+ return accessUriAsString(new URI(uri));
+ }
+
+ public static String accessUriAsString(URI uri) throws IOException, URISyntaxException {
+ InputStream stream = accessUri(uri);
+ return new Scanner(stream).useDelimiter("\\A").next();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/catalog/ValidatingConfig.java b/util/src/main/java/org/killbill/billing/util/config/catalog/ValidatingConfig.java
new file mode 100644
index 0000000..dc9ad83
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/catalog/ValidatingConfig.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config.catalog;
+
+import java.net.URI;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public abstract class ValidatingConfig<Context> {
+ /**
+ * All must implement validation
+ *
+ * @param root
+ * @param errors
+ * @return
+ */
+ public abstract ValidationErrors validate(Context root, ValidationErrors errors);
+
+
+ /**
+ * Override to initialize
+ *
+ * @param root
+ */
+ public void initialize(final Context root, final URI uri) {
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/catalog/ValidationError.java b/util/src/main/java/org/killbill/billing/util/config/catalog/ValidationError.java
new file mode 100644
index 0000000..f969651
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/catalog/ValidationError.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.util.config.catalog;
+
+import java.net.URI;
+
+import org.slf4j.Logger;
+
+
+public class ValidationError {
+ private final String description;
+ private final URI sourceURI;
+ private final Class<?> objectType;
+ private final String objectName;
+
+ public ValidationError(final String description, final URI sourceURI,
+ final Class<?> objectType, final String objectName) {
+ super();
+ this.description = description;
+ this.sourceURI = sourceURI;
+ this.objectType = objectType;
+ this.objectName = objectName;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public URI getSourceURI() {
+ return sourceURI;
+ }
+
+ public Class<?> getObjectType() {
+ return objectType;
+ }
+
+ public String getObjectName() {
+ return objectName;
+ }
+
+ public void log(final Logger log) {
+ log.error(String.format("%s [%s] (%s:%s)", description, sourceURI, objectType, objectName));
+ }
+
+ public String toString() {
+ return String.format("%s [%s] (%s:%s)\n", description, sourceURI, objectType, objectName);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/catalog/ValidationErrors.java b/util/src/main/java/org/killbill/billing/util/config/catalog/ValidationErrors.java
new file mode 100644
index 0000000..2cdb0f9
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/catalog/ValidationErrors.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config.catalog;
+
+import java.net.URI;
+import java.util.ArrayList;
+
+import org.slf4j.Logger;
+
+public class ValidationErrors extends ArrayList<ValidationError> {
+ private static final long serialVersionUID = 1L;
+
+ public void add(final String description, final URI catalogURI,
+ final Class<?> objectType, final String objectName) {
+ add(new ValidationError(description, catalogURI, objectType, objectName));
+
+ }
+
+ public void log(final Logger log) {
+ for (final ValidationError error : this) {
+ error.log(log);
+ }
+ }
+
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ for (final ValidationError error : this) {
+ builder.append(error.toString());
+ }
+ return builder.toString();
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/catalog/ValidationException.java b/util/src/main/java/org/killbill/billing/util/config/catalog/ValidationException.java
new file mode 100644
index 0000000..2e9a7d8
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/catalog/ValidationException.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config.catalog;
+
+import java.io.PrintStream;
+
+public class ValidationException extends Exception {
+ private final ValidationErrors errors;
+
+ ValidationException(final ValidationErrors errors) {
+ this.errors = errors;
+ }
+
+ public ValidationErrors getErrors() {
+ return errors;
+ }
+
+ @Override
+ public void printStackTrace(final PrintStream arg0) {
+ arg0.print(errors.toString());
+ super.printStackTrace(arg0);
+ }
+
+
+}
+
diff --git a/util/src/main/java/org/killbill/billing/util/config/catalog/XMLLoader.java b/util/src/main/java/org/killbill/billing/util/config/catalog/XMLLoader.java
new file mode 100644
index 0000000..6a2a4df
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/catalog/XMLLoader.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config.catalog;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.SAXException;
+
+import org.killbill.billing.catalog.api.InvalidConfigException;
+
+public class XMLLoader {
+ public static Logger log = LoggerFactory.getLogger(XMLLoader.class);
+
+ public static <T extends ValidatingConfig<T>> T getObjectFromString(final String uri, final Class<T> objectType) throws Exception {
+ if (uri == null) {
+ return null;
+ }
+ log.info("Initializing an object of class " + objectType.getName() + " from xml file at: " + uri);
+
+ return getObjectFromStream(new URI(uri), UriAccessor.accessUri(uri), objectType);
+ }
+
+ public static <T extends ValidatingConfig<T>> T getObjectFromUri(final URI uri, final Class<T> objectType) throws Exception {
+ if (uri == null) {
+ return null;
+ }
+ log.info("Initializing an object of class " + objectType.getName() + " from xml file at: " + uri);
+
+ return getObjectFromStream(uri, UriAccessor.accessUri(uri), objectType);
+ }
+
+ public static <T extends ValidatingConfig<T>> T getObjectFromStream(final URI uri, final InputStream stream, final Class<T> clazz) throws SAXException, InvalidConfigException, JAXBException, IOException, TransformerException, ValidationException {
+ if (stream == null) {
+ return null;
+ }
+
+ final Object o = unmarshaller(clazz).unmarshal(stream);
+ if (clazz.isInstance(o)) {
+ @SuppressWarnings("unchecked") final
+ T castObject = (T) o;
+ try {
+ validate(uri, castObject);
+ } catch (ValidationException e) {
+ e.getErrors().log(log);
+ System.err.println(e.getErrors().toString());
+ throw e;
+ }
+ return castObject;
+ } else {
+ return null;
+ }
+ }
+
+ public static <T> T getObjectFromStreamNoValidation(final InputStream stream, final Class<T> clazz) throws SAXException, InvalidConfigException, JAXBException, IOException, TransformerException {
+ final Object o = unmarshaller(clazz).unmarshal(stream);
+ if (clazz.isInstance(o)) {
+ @SuppressWarnings("unchecked") final
+ T castObject = (T) o;
+ return castObject;
+ } else {
+ return null;
+ }
+ }
+
+
+ public static <T extends ValidatingConfig<T>> void validate(final URI uri, final T c) throws ValidationException {
+ c.initialize(c, uri);
+ final ValidationErrors errs = c.validate(c, new ValidationErrors());
+ log.info("Errors: " + errs.size() + " for " + uri);
+ if (errs.size() > 0) {
+ throw new ValidationException(errs);
+ }
+ }
+
+ public static Unmarshaller unmarshaller(final Class<?> clazz) throws JAXBException, SAXException, IOException, TransformerException {
+ final JAXBContext context = JAXBContext.newInstance(clazz);
+
+ final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ final Unmarshaller um = context.createUnmarshaller();
+
+ final Schema schema = factory.newSchema(new StreamSource(XMLSchemaGenerator.xmlSchema(clazz)));
+ um.setSchema(schema);
+
+ return um;
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/catalog/XMLSchemaGenerator.java b/util/src/main/java/org/killbill/billing/util/config/catalog/XMLSchemaGenerator.java
new file mode 100644
index 0000000..b98b08e
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/catalog/XMLSchemaGenerator.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config.catalog;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.SchemaOutputResolver;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Result;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.w3c.dom.Document;
+
+public class XMLSchemaGenerator {
+ private static final int MAX_SCHEMA_SIZE_IN_BYTES = 100000;
+
+
+ //Note: this main method is called by the maven build to generate the schema for the jar
+ public static void main(final String[] args) throws IOException, TransformerException, JAXBException, ClassNotFoundException {
+ if (args.length != 2) {
+ printUsage();
+ System.exit(0);
+ }
+ final Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass(args[1]);
+
+ final JAXBContext context = JAXBContext.newInstance(clazz);
+ String xsdFileName = "Schema.xsd";
+ if (args.length != 0) {
+ xsdFileName = args[0] + "/" + xsdFileName;
+ }
+ final FileOutputStream s = new FileOutputStream(xsdFileName);
+ pojoToXSD(context, s);
+ }
+
+ private static void printUsage() {
+ System.out.println(XMLSchemaGenerator.class.getName() + " <file> <class1>");
+
+ }
+
+ public static String xmlSchemaAsString(final Class<?> clazz) throws IOException, TransformerException, JAXBException {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream(MAX_SCHEMA_SIZE_IN_BYTES);
+ final JAXBContext context = JAXBContext.newInstance(clazz);
+ pojoToXSD(context, output);
+ return new String(output.toByteArray());
+ }
+
+ public static InputStream xmlSchema(final Class<?> clazz) throws IOException, TransformerException, JAXBException {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream(MAX_SCHEMA_SIZE_IN_BYTES);
+ final JAXBContext context = JAXBContext.newInstance(clazz);
+ pojoToXSD(context, output);
+ return new ByteArrayInputStream(output.toByteArray());
+ }
+
+ public static void pojoToXSD(final JAXBContext context, final OutputStream out)
+ throws IOException, TransformerException {
+ final List<DOMResult> results = new ArrayList<DOMResult>();
+
+ context.generateSchema(new SchemaOutputResolver() {
+ @Override
+ public Result createOutput(final String ns, final String file)
+ throws IOException {
+ final DOMResult result = new DOMResult();
+ result.setSystemId(file);
+ results.add(result);
+ return result;
+ }
+ });
+
+ final DOMResult domResult = results.get(0);
+ final Document doc = (Document) domResult.getNode();
+
+ // Use a Transformer for output
+ final TransformerFactory tFactory = TransformerFactory.newInstance();
+ final Transformer transformer = tFactory.newTransformer();
+
+ final DOMSource source = new DOMSource(doc);
+ final StreamResult result = new StreamResult(out);
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.transform(source, result);
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/catalog/XMLWriter.java b/util/src/main/java/org/killbill/billing/util/config/catalog/XMLWriter.java
new file mode 100644
index 0000000..5e5678f
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/catalog/XMLWriter.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config.catalog;
+
+import java.io.ByteArrayOutputStream;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.Marshaller;
+
+public class XMLWriter<T> {
+ private static final int MAX_XML_SIZE_IN_BYTES = 100000;
+
+ public static <T> String writeXML(final T object, final Class<T> type) throws Exception {
+ final JAXBContext context = JAXBContext.newInstance(type);
+ final Marshaller marshaller = context.createMarshaller();
+ marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
+ final ByteArrayOutputStream output = new ByteArrayOutputStream(MAX_XML_SIZE_IN_BYTES);
+
+ marshaller.marshal(object, output);
+
+ return new String(output.toByteArray());
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/CatalogConfig.java b/util/src/main/java/org/killbill/billing/util/config/CatalogConfig.java
new file mode 100644
index 0000000..28c09ef
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/CatalogConfig.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+public interface CatalogConfig extends KillbillConfig {
+
+ @Config("org.killbill.catalog.uri")
+ @Default("SpyCarBasic.xml")
+ @Description("Catalog location. Either in the classpath or in the filesystem")
+ String getCatalogURI();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/CurrencyConfig.java b/util/src/main/java/org/killbill/billing/util/config/CurrencyConfig.java
new file mode 100644
index 0000000..9646dc9
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/CurrencyConfig.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+public interface CurrencyConfig extends KillbillConfig {
+
+ @Config("org.killbill.currency.provider.default")
+ @Default("killbill-currency-plugin")
+ @Description("Default currency provider to use")
+ public String getDefaultCurrencyProvider();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java b/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java
new file mode 100644
index 0000000..cd71a12
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/InvoiceConfig.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+public interface InvoiceConfig extends KillbillConfig {
+
+ @Config("org.killbill.invoice.maxNumberOfMonthsInFuture")
+ @Default("36")
+ @Description("Maximum target date to consider when generating an invoice")
+ public int getNumberOfMonthsInFuture();
+
+ @Config("org.killbill.invoice.emailNotificationsEnabled")
+ @Default("false")
+ @Description("Whether to send email notifications on invoice creation (for configured accounts)")
+ public boolean isEmailNotificationsEnabled();
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/KillbillConfig.java b/util/src/main/java/org/killbill/billing/util/config/KillbillConfig.java
new file mode 100644
index 0000000..a1f6802
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/KillbillConfig.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.util.config;
+
+/*
+ * Marker interface for killbill config files
+ */
+public interface KillbillConfig {
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/OSGIConfig.java b/util/src/main/java/org/killbill/billing/util/config/OSGIConfig.java
new file mode 100644
index 0000000..0107e83
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/OSGIConfig.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+
+public interface OSGIConfig extends KillbillConfig {
+
+ @Config("org.killbill.osgi.bundle.property.name")
+ @Default("killbill.properties")
+ @Description("Name of the properties file for OSGI plugins")
+ public String getOSGIKillbillPropertyName();
+
+ @Config("org.killbill.osgi.root.dir")
+ @Default("/var/tmp/felix")
+ @Description("Bundles cache area for the OSGI framework")
+ public String getOSGIBundleRootDir();
+
+ @Config("org.killbill.osgi.bundle.cache.name")
+ @Default("osgi-cache")
+ @Description("Bundles cache name")
+ public String getOSGIBundleCacheName();
+
+ @Config("org.killbill.osgi.bundle.install.dir")
+ @Default("/var/tmp/bundles")
+ @Description("Bundles install directory")
+ public String getRootInstallationDir();
+
+ @Config("org.killbill.osgi.system.bundle.export.packages")
+ @Default("org.killbill.billing.account.api," +
+ "org.killbill.billing.analytics.api.sanity," +
+ "org.killbill.billing.analytics.api.user," +
+ "org.killbill.billing.beatrix.bus.api," + /* TODO PIERRE Remove it after plugins classes have been regenerated */
+ "org.killbill.billing.catalog.api," +
+ "org.killbill.billing.invoice.api," +
+ "org.killbill.billing.entitlement.api," +
+ "org.killbill.billing," +
+ "org.killbill.billing.notification.api," +
+ "org.killbill.billing.notification.plugin.api," +
+ "org.killbill.billing.osgi.api," +
+ "org.killbill.billing.osgi.api.config," +
+ "org.killbill.billing.overdue," +
+ "org.killbill.billing.payment.api," +
+ "org.killbill.billing.payment.plugin.api," +
+ "org.killbill.billing.tenant.api," +
+ "org.killbill.billing.usage.api," +
+ "org.killbill.billing.util.api," +
+ "org.killbill.billing.util.audit," +
+ "org.killbill.billing.util.callcontext," +
+ "org.killbill.billing.util.customfield," +
+ "org.killbill.billing.notification.plugin," +
+ "org.killbill.billing.currency.plugin.api," +
+ "org.killbill.billing.currency.api," +
+ "org.killbill.billing.util.email," +
+ "org.killbill.billing.util.entity," +
+ "org.killbill.billing.util.tag," +
+ "org.killbill.billing.util.template," +
+ "org.killbill.billing.util.template.translation," +
+ // javax.servlet and javax.servlet.http are not exported by default - we
+ // need the bundles to see them for them to be able to register their servlets.
+ // Note: bundles should mark javax.servlet:servlet-api as provided
+ "sun.misc," +
+ "sun.misc.unsafe," +
+ "javax.crypto," +
+ "javax.crypto.spec," +
+ "javax.management," +
+ "javax.servlet;version=3.0," +
+ "javax.servlet.http;version=3.0," +
+ // Since we are using joda in our APIs we need to export it
+ "org.joda.time;org.joda.time.format;version=2.3," +
+
+ "org.osgi.service.log;version=1.3," +
+ // Let the world know the System bundle exposes (via org.osgi.compendium) the requirement (osgi.wiring.package=org.osgi.service.http)
+ "org.osgi.service.http," +
+ // Let the world know the System bundle exposes (via org.osgi.compendium) the requirement (&(osgi.wiring.package=org.osgi.service.deploymentadmin)(version>=1.1.0)(!(version>=2.0.0)))
+ "org.osgi.service.deploymentadmin;version=1.1.0," +
+ // Let the world know the System bundle exposes (via org.osgi.compendium) the requirement (&(osgi.wiring.package=org.osgi.service.event)(version>=1.2.0)(!(version>=2.0.0)))
+ "org.osgi.service.event;version=1.2.0," +
+ // Let the world know the System bundle exposes the requirement (&(osgi.wiring.package=org.slf4j)(version>=1.7.0)(!(version>=2.0.0)))
+ "org.slf4j;version=1.7.2")
+ @Description("Packages to export from the system bundle")
+ public String getSystemBundleExportPackages();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java b/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java
new file mode 100644
index 0000000..da7f219
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/PaymentConfig.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import java.util.List;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+import org.skife.config.TimeSpan;
+
+public interface PaymentConfig extends KillbillConfig {
+
+ @Config("org.killbill.payment.provider.default")
+ // See ExternalPaymentProviderPlugin.PLUGIN_NAME
+ @Default("__external_payment__")
+ @Description("Default payment provider to use")
+ public String getDefaultPaymentProvider();
+
+ @Config("org.killbill.payment.retry.days")
+ @Default("8,8,8")
+ @Description("Interval in days between payment retries")
+ public List<Integer> getPaymentRetryDays();
+
+ @Config("orgkillbill.payment.failure.retry.start.sec")
+ @Default("300")
+ public int getPluginFailureRetryStart();
+
+ @Config("org.killbill.payment.failure.retry.multiplier")
+ @Default("2")
+ public int getPluginFailureRetryMultiplier();
+
+ @Config("org.killbill.payment.failure.retry.max.attempts")
+ @Default("8")
+ @Description("Maximum number of retries for failed payments")
+ public int getPluginFailureRetryMaxAttempts();
+
+ @Config("org.killbill.payment.plugin.timeout")
+ @Default("90s")
+ @Description("Timeout for each payment attempt")
+ public TimeSpan getPaymentPluginTimeout();
+
+ @Config("org.killbill.payment.plugin.threads.nb")
+ @Default("10")
+ @Description("Number of threads for plugin executor dispatcher")
+ public int getPaymentPluginThreadNb();
+
+ @Config("org.killbill.payment.off")
+ @Default("false")
+ @Description("Whether the payment subsystem is off")
+ public boolean isPaymentOff();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/RbacConfig.java b/util/src/main/java/org/killbill/billing/util/config/RbacConfig.java
new file mode 100644
index 0000000..900aeff
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/RbacConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+import org.skife.config.TimeSpan;
+
+public interface RbacConfig extends KillbillConfig {
+
+ @Config("org.killbill.rbac.globalSessionTimeout")
+ @Default("1h")
+ @Description("System-wide default time that any session may remain idle before expiring")
+ public TimeSpan getGlobalSessionTimeout();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java b/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java
new file mode 100644
index 0000000..d53b6c6
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.DefaultNull;
+import org.skife.config.Description;
+
+public interface SecurityConfig extends KillbillConfig {
+
+ @Config("org.killbill.security.shiroResourcePath")
+ @Default("classpath:shiro.ini")
+ @Description("Path to the shiro.ini file (classpath, url or file resource)")
+ public String getShiroResourcePath();
+
+ // LDAP Realm
+
+ @Config("org.killbill.security.ldap.userDnTemplate")
+ @DefaultNull
+ @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.searchBase")
+ @DefaultNull
+ @Description("LDAP search base to use")
+ public String getShiroLDAPSearchBase();
+
+ @Config("org.killbill.security.ldap.groupSearchFilter")
+ @Default("memberOf=uid={0}")
+ @Description("LDAP search filter to use to find groups (e.g. memberOf=uid={0},ou=users,dc=mycompany,dc=com)")
+ public String getShiroLDAPGroupSearchFilter();
+
+ @Config("org.killbill.security.ldap.groupNameId")
+ @Default("memberOf")
+ @Description("Group name attribute ID in LDAP")
+ public String getShiroLDAPGroupNameID();
+
+ @Config("org.killbill.security.ldap.permissionsByGroup")
+ @Default("admin = *:*\n" +
+ "finance = invoice:*, payment:*\n" +
+ "support = entitlement:*, invoice:item_adjust")
+ @Description("LDAP permissions by LDAP group")
+ public String getShiroLDAPPermissionsByGroup();
+
+ @Config("org.killbill.security.ldap.url")
+ @Default("ldap://127.0.0.1:389")
+ @Description("LDAP server url")
+ public String getShiroLDAPUrl();
+
+ @Config("org.killbill.security.ldap.systemUsername")
+ @DefaultNull
+ @Description("LDAP username")
+ public String getShiroLDAPSystemUsername();
+
+ @Config("org.killbill.security.ldap.systemPassword")
+ @DefaultNull
+ @Description("LDAP password")
+ public String getShiroLDAPSystemPassword();
+
+ @Config("org.killbill.security.ldap.authenticationMechanism")
+ @Default("simple")
+ @Description("LDAP authentication mechanism (e.g. DIGEST-MD5)")
+ public String getShiroLDAPAuthenticationMechanism();
+
+ @Config("org.killbill.security.ldap.disableSSLCheck")
+ @Default("false")
+ @Description("Whether to ignore SSL certificates checks")
+ public boolean disableShiroLDAPSSLCheck();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/currency/KillBillMoney.java b/util/src/main/java/org/killbill/billing/util/currency/KillBillMoney.java
new file mode 100644
index 0000000..297b941
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/currency/KillBillMoney.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.currency;
+
+import java.math.BigDecimal;
+
+import org.joda.money.CurrencyUnit;
+
+import org.killbill.billing.catalog.api.Currency;
+
+public class KillBillMoney {
+
+ public static final int ROUNDING_METHOD = BigDecimal.ROUND_HALF_UP;
+ public static final int MAX_SCALE = 9;
+
+ private KillBillMoney() {}
+
+ public static BigDecimal of(final BigDecimal amount, final Currency currency) {
+ final CurrencyUnit currencyUnit = CurrencyUnit.getInstance(currency.toString());
+ return amount.setScale(currencyUnit.getDecimalPlaces(), ROUNDING_METHOD);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldCreationEvent.java b/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldCreationEvent.java
new file mode 100644
index 0000000..97e131d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldCreationEvent.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.api;
+
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.CustomFieldCreationEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultCustomFieldCreationEvent extends BusEventBase implements CustomFieldCreationEvent {
+
+ private final UUID customFieldId;
+ private final UUID objectId;
+ private final ObjectType objectType;
+
+ @JsonCreator
+ public DefaultCustomFieldCreationEvent(@JsonProperty("customFieldId") final UUID customFieldId,
+ @JsonProperty("objectId") final UUID objectId,
+ @JsonProperty("objectType") final ObjectType objectType,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.customFieldId = customFieldId;
+ this.objectId = objectId;
+ this.objectType = objectType;
+ }
+
+ @Override
+ public UUID getCustomFieldId() {
+ return customFieldId;
+ }
+
+ @Override
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ @Override
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.CUSTOM_FIELD_CREATION;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DefaultCustomFieldCreationEvent)) {
+ return false;
+ }
+
+ final DefaultCustomFieldCreationEvent that = (DefaultCustomFieldCreationEvent) o;
+
+ if (customFieldId != null ? !customFieldId.equals(that.customFieldId) : that.customFieldId != null) {
+ return false;
+ }
+ if (objectId != null ? !objectId.equals(that.objectId) : that.objectId != null) {
+ return false;
+ }
+ if (objectType != that.objectType) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = customFieldId != null ? customFieldId.hashCode() : 0;
+ result = 31 * result + (objectId != null ? objectId.hashCode() : 0);
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldDeletionEvent.java b/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldDeletionEvent.java
new file mode 100644
index 0000000..a2c334a
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldDeletionEvent.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.api;
+
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.CustomFieldDeletionEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultCustomFieldDeletionEvent extends BusEventBase implements CustomFieldDeletionEvent {
+
+ private final UUID customFieldId;
+ private final UUID objectId;
+ private final ObjectType objectType;
+
+ @JsonCreator
+ public DefaultCustomFieldDeletionEvent(@JsonProperty("customFieldId") final UUID customFieldId,
+ @JsonProperty("objectId") final UUID objectId,
+ @JsonProperty("objectType") final ObjectType objectType,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.customFieldId = customFieldId;
+ this.objectId = objectId;
+ this.objectType = objectType;
+ }
+
+ @Override
+ public UUID getCustomFieldId() {
+ return customFieldId;
+ }
+
+ @Override
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ @Override
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.CUSTOM_FIELD_DELETION;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DefaultCustomFieldDeletionEvent)) {
+ return false;
+ }
+
+ final DefaultCustomFieldDeletionEvent that = (DefaultCustomFieldDeletionEvent) o;
+
+ if (customFieldId != null ? !customFieldId.equals(that.customFieldId) : that.customFieldId != null) {
+ return false;
+ }
+ if (objectId != null ? !objectId.equals(that.objectId) : that.objectId != null) {
+ return false;
+ }
+ if (objectType != that.objectType) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = customFieldId != null ? customFieldId.hashCode() : 0;
+ result = 31 * result + (objectId != null ? objectId.hashCode() : 0);
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldUserApi.java b/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldUserApi.java
new file mode 100644
index 0000000..ad05b91
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/customfield/api/DefaultCustomFieldUserApi.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.api;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.customfield.CustomField;
+import org.killbill.billing.util.customfield.StringCustomField;
+import org.killbill.billing.util.customfield.dao.CustomFieldDao;
+import org.killbill.billing.util.customfield.dao.CustomFieldModelDao;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;
+
+public class DefaultCustomFieldUserApi implements CustomFieldUserApi {
+
+ private static final Function<CustomFieldModelDao, CustomField> CUSTOM_FIELD_MODEL_DAO_CUSTOM_FIELD_FUNCTION = new Function<CustomFieldModelDao, CustomField>() {
+ @Override
+ public CustomField apply(final CustomFieldModelDao input) {
+ return new StringCustomField(input);
+ }
+ };
+
+ private final InternalCallContextFactory internalCallContextFactory;
+ private final CustomFieldDao customFieldDao;
+
+ @Inject
+ public DefaultCustomFieldUserApi(final InternalCallContextFactory internalCallContextFactory, final CustomFieldDao customFieldDao) {
+ this.internalCallContextFactory = internalCallContextFactory;
+ this.customFieldDao = customFieldDao;
+ }
+
+ @Override
+ public Pagination<CustomField> searchCustomFields(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
+ return getEntityPaginationNoException(limit,
+ new SourcePaginationBuilder<CustomFieldModelDao, CustomFieldApiException>() {
+ @Override
+ public Pagination<CustomFieldModelDao> build() {
+ return customFieldDao.searchCustomFields(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+ }
+ },
+ CUSTOM_FIELD_MODEL_DAO_CUSTOM_FIELD_FUNCTION);
+ }
+
+ @Override
+ public Pagination<CustomField> getCustomFields(final Long offset, final Long limit, final TenantContext context) {
+ return getEntityPaginationNoException(limit,
+ new SourcePaginationBuilder<CustomFieldModelDao, CustomFieldApiException>() {
+ @Override
+ public Pagination<CustomFieldModelDao> build() {
+ return customFieldDao.get(offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+ }
+ },
+ CUSTOM_FIELD_MODEL_DAO_CUSTOM_FIELD_FUNCTION);
+ }
+
+ @Override
+ public void addCustomFields(final List<CustomField> customFields, final CallContext context) throws CustomFieldApiException {
+ // TODO make it transactional
+
+ final Map<UUID, ObjectType> mapping = new HashMap<UUID, ObjectType>();
+ for (final CustomField cur : customFields) {
+ mapping.put(cur.getObjectId(), cur.getObjectType());
+ }
+
+ final List<CustomFieldModelDao> all = new LinkedList<CustomFieldModelDao>();
+ for (UUID cur : mapping.keySet()) {
+ final ObjectType type = mapping.get(cur);
+ all.addAll(customFieldDao.getCustomFieldsForObject(cur, type, internalCallContextFactory.createInternalCallContext(cur, type, context)));
+ }
+ final List<CustomField> toBeInserted = new LinkedList<CustomField>();
+ for (final CustomField cur : customFields) {
+ final CustomFieldModelDao match = Iterables.tryFind(all, new com.google.common.base.Predicate<CustomFieldModelDao>() {
+ @Override
+ public boolean apply(final CustomFieldModelDao input) {
+ return input.getObjectId().equals(cur.getObjectId()) &&
+ input.getObjectType() == cur.getObjectType() &&
+ input.getFieldName().equals(cur.getFieldName());
+
+ }
+ }).orNull();
+ if (match != null) {
+ throw new CustomFieldApiException(ErrorCode.CUSTOM_FIELD_ALREADY_EXISTS, match.getId());
+ }
+ toBeInserted.add(cur);
+ }
+
+ for (CustomField cur : toBeInserted) {
+ customFieldDao.create(new CustomFieldModelDao(cur), internalCallContextFactory.createInternalCallContext(cur.getObjectId(), cur.getObjectType(), context));
+ }
+ }
+
+ @Override
+ public void removeCustomFields(final List<CustomField> customFields, final CallContext context) throws CustomFieldApiException {
+ // TODO make it transactional
+ for (final CustomField cur : customFields) {
+ customFieldDao.deleteCustomField(cur.getId(), internalCallContextFactory.createInternalCallContext(cur.getObjectId(), cur.getObjectType(), context));
+ }
+ }
+
+ @Override
+ public List<CustomField> getCustomFieldsForObject(final UUID objectId, final ObjectType objectType, final TenantContext context) {
+ return withCustomFieldsTransform(customFieldDao.getCustomFieldsForObject(objectId, objectType, internalCallContextFactory.createInternalTenantContext(context)));
+ }
+
+ @Override
+ public List<CustomField> getCustomFieldsForAccountType(final UUID accountId, final ObjectType objectType, final TenantContext context) {
+ return withCustomFieldsTransform(customFieldDao.getCustomFieldsForAccountType(objectType, internalCallContextFactory.createInternalTenantContext(accountId, context)));
+ }
+
+ @Override
+ public List<CustomField> getCustomFieldsForAccount(final UUID accountId, final TenantContext context) {
+ return withCustomFieldsTransform(customFieldDao.getCustomFieldsForAccount(internalCallContextFactory.createInternalTenantContext(accountId, context)));
+ }
+
+ private List<CustomField> withCustomFieldsTransform(final Collection<CustomFieldModelDao> input) {
+ return ImmutableList.<CustomField>copyOf(Collections2.transform(input, CUSTOM_FIELD_MODEL_DAO_CUSTOM_FIELD_FUNCTION));
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldDao.java b/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldDao.java
new file mode 100644
index 0000000..977ddaa
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldDao.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.customfield.CustomField;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.EntityDao;
+
+public interface CustomFieldDao extends EntityDao<CustomFieldModelDao, CustomField, CustomFieldApiException> {
+
+ public Pagination<CustomFieldModelDao> searchCustomFields(String searchKey, Long offset, Long limit, InternalTenantContext context);
+
+ public List<CustomFieldModelDao> getCustomFieldsForObject(final UUID objectId, final ObjectType objectType, final InternalTenantContext context);
+
+ public List<CustomFieldModelDao> getCustomFieldsForAccountType(final ObjectType objectType, final InternalTenantContext context);
+
+ public List<CustomFieldModelDao> getCustomFieldsForAccount(final InternalTenantContext context);
+
+ void deleteCustomField(UUID customFieldId, InternalCallContext context) throws CustomFieldApiException;
+}
diff --git a/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldModelDao.java b/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldModelDao.java
new file mode 100644
index 0000000..3987b3e
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldModelDao.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.dao;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.customfield.CustomField;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class CustomFieldModelDao extends EntityBase implements EntityModelDao<CustomField> {
+
+ private String fieldName;
+ private String fieldValue;
+ private UUID objectId;
+ private ObjectType objectType;
+ private Boolean isActive;
+
+
+ public CustomFieldModelDao() { /* For the DAO mapper */ }
+
+ public CustomFieldModelDao(final UUID id, final DateTime createdDate, final DateTime updatedDate, final String fieldName,
+ final String fieldValue, final UUID objectId, final ObjectType objectType) {
+ super(id, createdDate, updatedDate);
+ this.fieldName = fieldName;
+ this.fieldValue = fieldValue;
+ this.objectId = objectId;
+ this.objectType = objectType;
+ this.isActive = true;
+ }
+
+ public CustomFieldModelDao(final CustomField customField) {
+ this(customField.getId(), customField.getCreatedDate(), customField.getUpdatedDate(), customField.getFieldName(),
+ customField.getFieldValue(), customField.getObjectId(), customField.getObjectType());
+ }
+
+ public String getFieldName() {
+ return fieldName;
+ }
+
+ public String getFieldValue() {
+ return fieldValue;
+ }
+
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ public void setFieldName(final String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ public void setFieldValue(final String fieldValue) {
+ this.fieldValue = fieldValue;
+ }
+
+ public void setObjectId(final UUID objectId) {
+ this.objectId = objectId;
+ }
+
+ public void setObjectType(final ObjectType objectType) {
+ this.objectType = objectType;
+ }
+
+ public Boolean getIsActive() {
+ return isActive;
+ }
+
+ public void setIsActive(final Boolean isActive) {
+ this.isActive = isActive;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("CustomFieldModelDao");
+ sb.append("{fieldName='").append(fieldName).append('\'');
+ sb.append(", fieldValue='").append(fieldValue).append('\'');
+ sb.append(", objectId=").append(objectId);
+ sb.append(", objectType=").append(objectType);
+ sb.append(", isActive=").append(isActive);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final CustomFieldModelDao that = (CustomFieldModelDao) o;
+
+ if (fieldName != null ? !fieldName.equals(that.fieldName) : that.fieldName != null) {
+ return false;
+ }
+ if (fieldValue != null ? !fieldValue.equals(that.fieldValue) : that.fieldValue != null) {
+ return false;
+ }
+ if (objectId != null ? !objectId.equals(that.objectId) : that.objectId != null) {
+ return false;
+ }
+ if (objectType != that.objectType) {
+ return false;
+ }
+ if (isActive != null ? !isActive.equals(that.isActive) : that.isActive != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (fieldName != null ? fieldName.hashCode() : 0);
+ result = 31 * result + (fieldValue != null ? fieldValue.hashCode() : 0);
+ result = 31 * result + (objectId != null ? objectId.hashCode() : 0);
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ result = 31 * result + (isActive != null ? isActive.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.CUSTOM_FIELD;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return TableName.CUSTOM_FIELD_HISTORY;
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldSqlDao.java b/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldSqlDao.java
new file mode 100644
index 0000000..53e589e
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/customfield/dao/CustomFieldSqlDao.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.customfield.CustomField;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+@EntitySqlDaoStringTemplate
+public interface CustomFieldSqlDao extends EntitySqlDao<CustomFieldModelDao, CustomField> {
+
+ @SqlUpdate
+ @Audited(ChangeType.DELETE)
+ void markTagAsDeleted(@Bind("id") String customFieldId,
+ @BindBean InternalCallContext context);
+
+ @SqlQuery
+ List<CustomFieldModelDao> getCustomFieldsForObject(@Bind("objectId") UUID objectId,
+ @Bind("objectType") ObjectType objectType,
+ @BindBean InternalTenantContext internalTenantContext);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/customfield/dao/DefaultCustomFieldDao.java b/util/src/main/java/org/killbill/billing/util/customfield/dao/DefaultCustomFieldDao.java
new file mode 100644
index 0000000..0791c2d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/customfield/dao/DefaultCustomFieldDao.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.dao;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.skife.jdbi.v2.IDBI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.customfield.CustomField;
+import org.killbill.billing.util.customfield.api.DefaultCustomFieldCreationEvent;
+import org.killbill.billing.util.customfield.api.DefaultCustomFieldDeletionEvent;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
+import org.killbill.billing.util.entity.dao.EntityDaoBase;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+public class DefaultCustomFieldDao extends EntityDaoBase<CustomFieldModelDao, CustomField, CustomFieldApiException> implements CustomFieldDao {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultCustomFieldDao.class);
+
+ private final PersistentBus bus;
+
+ @Inject
+ public DefaultCustomFieldDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher controllerDispatcher, final NonEntityDao nonEntityDao, final PersistentBus bus) {
+ super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, controllerDispatcher, nonEntityDao), CustomFieldSqlDao.class);
+ this.bus = bus;
+ }
+
+ @Override
+ public List<CustomFieldModelDao> getCustomFieldsForObject(final UUID objectId, final ObjectType objectType, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<CustomFieldModelDao>>() {
+ @Override
+ public List<CustomFieldModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(CustomFieldSqlDao.class).getCustomFieldsForObject(objectId, objectType, context);
+ }
+ });
+ }
+
+ @Override
+ public List<CustomFieldModelDao> getCustomFieldsForAccountType(final ObjectType objectType, final InternalTenantContext context) {
+ final List<CustomFieldModelDao> allFields = getCustomFieldsForAccount(context);
+
+ return ImmutableList.<CustomFieldModelDao>copyOf(Collections2.filter(allFields, new Predicate<CustomFieldModelDao>() {
+ @Override
+ public boolean apply(@Nullable final CustomFieldModelDao input) {
+ return input.getObjectType() == objectType;
+ }
+ }));
+ }
+
+ @Override
+ public List<CustomFieldModelDao> getCustomFieldsForAccount(final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<CustomFieldModelDao>>() {
+ @Override
+ public List<CustomFieldModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(CustomFieldSqlDao.class).getByAccountRecordId(context);
+ }
+ });
+ }
+
+ @Override
+ public void deleteCustomField(final UUID customFieldId, final InternalCallContext context) throws CustomFieldApiException {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ entitySqlDaoWrapperFactory.become(CustomFieldSqlDao.class).markTagAsDeleted(customFieldId.toString(), context);
+ return null;
+ }
+ });
+
+ }
+
+ @Override
+ protected CustomFieldApiException generateAlreadyExistsException(final CustomFieldModelDao entity, final InternalCallContext context) {
+ return new CustomFieldApiException(ErrorCode.CUSTOM_FIELD_ALREADY_EXISTS, entity.getId());
+ }
+
+ @Override
+ protected void postBusEventFromTransaction(final CustomFieldModelDao customField, final CustomFieldModelDao savedCustomField, final ChangeType changeType,
+ final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final InternalCallContext context)
+ throws BillingExceptionBase {
+
+ BusInternalEvent customFieldEvent = null;
+ switch (changeType) {
+ case INSERT:
+ customFieldEvent = new DefaultCustomFieldCreationEvent(customField.getId(), customField.getObjectId(), customField.getObjectType(),
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+ break;
+ case DELETE:
+ customFieldEvent = new DefaultCustomFieldDeletionEvent(customField.getId(), customField.getObjectId(), customField.getObjectType(),
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+ break;
+ default:
+ return;
+ }
+
+ try {
+ bus.postFromTransaction(customFieldEvent, entitySqlDaoWrapperFactory.getSqlDao());
+ } catch (PersistentBus.EventBusException e) {
+ log.warn("Failed to post tag event for custom field " + customField.getId().toString(), e);
+ }
+
+ }
+
+ @Override
+ public Pagination<CustomFieldModelDao> searchCustomFields(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+ return paginationHelper.getPagination(CustomFieldSqlDao.class,
+ new PaginationIteratorBuilder<CustomFieldModelDao, CustomField, CustomFieldSqlDao>() {
+ @Override
+ public Long getCount(final CustomFieldSqlDao customFieldSqlDao, final InternalTenantContext context) {
+ return customFieldSqlDao.getSearchCount(searchKey, String.format("%%%s%%", searchKey), context);
+ }
+
+ @Override
+ public Iterator<CustomFieldModelDao> build(final CustomFieldSqlDao customFieldSqlDao, final Long limit, final InternalTenantContext context) {
+ return customFieldSqlDao.search(searchKey, String.format("%%%s%%", searchKey), offset, limit, context);
+ }
+ },
+ offset,
+ limit,
+ context);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/customfield/ShouldntHappenException.java b/util/src/main/java/org/killbill/billing/util/customfield/ShouldntHappenException.java
new file mode 100644
index 0000000..523898e
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/customfield/ShouldntHappenException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield;
+
+public class ShouldntHappenException extends RuntimeException {
+
+ public ShouldntHappenException(final String message) {
+ super(message);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/customfield/StringCustomField.java b/util/src/main/java/org/killbill/billing/util/customfield/StringCustomField.java
new file mode 100644
index 0000000..16292f7
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/customfield/StringCustomField.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.customfield.dao.CustomFieldModelDao;
+import org.killbill.billing.entity.EntityBase;
+
+public class StringCustomField extends EntityBase implements CustomField {
+
+ private final String fieldName;
+ private final String fieldValue;
+ private final UUID objectId;
+ private final ObjectType objectType;
+
+ public StringCustomField(final String name, final String value, final ObjectType objectType, final UUID objectId, final DateTime createdDate) {
+ this(UUID.randomUUID(), name, value, objectType, objectId, createdDate);
+ }
+
+ public StringCustomField(final UUID id, final String name, final String value, final ObjectType objectType, final UUID objectId, final DateTime createdDate) {
+ super(id, createdDate, createdDate);
+ this.fieldName = name;
+ this.fieldValue = value;
+ this.objectId = objectId;
+ this.objectType = objectType;
+
+ }
+
+ public StringCustomField(final CustomFieldModelDao input) {
+ this(input.getId(), input.getFieldName(), input.getFieldValue(), input.getObjectType(), input.getObjectId(), input.getCreatedDate());
+ }
+
+ @Override
+ public String getFieldName() {
+ return fieldName;
+ }
+
+ @Override
+ public String getFieldValue() {
+ return fieldValue;
+ }
+
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("StringCustomField");
+ sb.append("{fieldName='").append(fieldName).append('\'');
+ sb.append(", fieldValue='").append(fieldValue).append('\'');
+ sb.append(", objectId=").append(objectId);
+ sb.append(", objectType=").append(objectType);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final StringCustomField that = (StringCustomField) o;
+
+ if (fieldName != null ? !fieldName.equals(that.fieldName) : that.fieldName != null) {
+ return false;
+ }
+ if (fieldValue != null ? !fieldValue.equals(that.fieldValue) : that.fieldValue != null) {
+ return false;
+ }
+ if (objectId != null ? !objectId.equals(that.objectId) : that.objectId != null) {
+ return false;
+ }
+ if (objectType != that.objectType) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (fieldName != null ? fieldName.hashCode() : 0);
+ result = 31 * result + (fieldValue != null ? fieldValue.hashCode() : 0);
+ result = 31 * result + (objectId != null ? objectId.hashCode() : 0);
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/AuditLogModelDaoMapper.java b/util/src/main/java/org/killbill/billing/util/dao/AuditLogModelDaoMapper.java
new file mode 100644
index 0000000..5bae32d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/AuditLogModelDaoMapper.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import org.killbill.billing.callcontext.DefaultCallContext;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.audit.dao.AuditLogModelDao;
+import org.killbill.billing.util.callcontext.CallContext;
+
+public class AuditLogModelDaoMapper extends MapperBase implements ResultSetMapper<AuditLogModelDao> {
+
+ @Override
+ public AuditLogModelDao map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+ final UUID id = getUUID(r, "id");
+ final String tableName = r.getString("table_name");
+ final long targetRecordId = r.getLong("target_record_id");
+ final String changeType = r.getString("change_type");
+ final DateTime createdDate = getDateTime(r, "created_date");
+ final String createdBy = r.getString("created_by");
+ final String reasonCode = r.getString("reason_code");
+ final String comments = r.getString("comments");
+ final UUID userToken = getUUID(r, "user_token");
+
+ final EntityAudit entityAudit = new EntityAudit(id, TableName.valueOf(tableName), targetRecordId, ChangeType.valueOf(changeType), createdDate);
+ // TODO - we have the tenant_record_id but not the tenant id here
+ final CallContext callContext = new DefaultCallContext(null, createdBy, createdDate, reasonCode, comments, userToken);
+ return new AuditLogModelDao(entityAudit, callContext);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/AuditSqlDao.java b/util/src/main/java/org/killbill/billing/util/dao/AuditSqlDao.java
new file mode 100644
index 0000000..30cee0c
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/AuditSqlDao.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.customizers.Define;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.commons.jdbi.statement.SmartFetchSize;
+import org.killbill.billing.util.audit.dao.AuditLogModelDao;
+import org.killbill.billing.util.cache.Cachable;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CachableKey;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+/**
+ * Note 1: cache invalidation has to happen for audit logs (which is tricky in the multi-nodes scenario).
+ * For now, we're using a time-based eviction strategy (see timeToIdleSeconds and timeToLiveSeconds in ehcache.xml)
+ * which is good enough: the cache will always get at least the initial CREATION audit log entry, which is the one
+ * we really care about (both for Analytics and for Kaui's endpoints). Besides, we do cache invalidation properly
+ * on our own node (see EntitySqlDaoWrapperInvocationHandler).
+ * <p/>
+ * Note 2: in the queries below, tableName always refers to the TableName enum, not the actual table name (TableName.getTableName()).
+ */
+@EntitySqlDaoStringTemplate("/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg")
+// Note: @RegisterMapper annotation won't work here as we build the SqlObject via EntitySqlDao (annotations won't be inherited for JDBI)
+public interface AuditSqlDao {
+
+ @SqlUpdate
+ public void insertAuditFromTransaction(@BindBean final EntityAudit audit,
+ @BindBean final InternalCallContext context);
+
+ @SqlQuery
+ @SmartFetchSize(shouldStream = true)
+ public Iterator<AuditLogModelDao> getAuditLogsForAccountRecordId(@BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @SmartFetchSize(shouldStream = true)
+ public Iterator<AuditLogModelDao> getAuditLogsForTableNameAndAccountRecordId(@Bind("tableName") final String tableName,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @Cachable(CacheType.AUDIT_LOG)
+ public List<AuditLogModelDao> getAuditLogsForTargetRecordId(@CachableKey(1) @Bind("tableName") final String tableName,
+ @CachableKey(2) @Bind("targetRecordId") final long targetRecordId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @Cachable(CacheType.AUDIT_LOG_VIA_HISTORY)
+ public List<AuditLogModelDao> getAuditLogsViaHistoryForTargetRecordId(@CachableKey(1) @Bind("tableName") final String historyTableName, /* Uppercased - used to find entries in audit_log table */
+ @CachableKey(2) @Define("historyTableName") final String actualHistoryTableName, /* Actual table name, used in the inner join query */
+ @CachableKey(3) @Bind("targetRecordId") final long targetRecordId,
+ @BindBean final InternalTenantContext context);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/BinderBase.java b/util/src/main/java/org/killbill/billing/util/dao/BinderBase.java
new file mode 100644
index 0000000..53cf6b3
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/BinderBase.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.util.Date;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+public abstract class BinderBase {
+
+ protected Date getDate(final DateTime dateTime) {
+ return dateTime == null ? null : dateTime.toDate();
+ }
+
+ protected String getUUIDString(final UUID uuid) {
+ return uuid == null ? null : uuid.toString();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/DateTimeArgumentFactory.java b/util/src/main/java/org/killbill/billing/util/dao/DateTimeArgumentFactory.java
new file mode 100644
index 0000000..382527b
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/DateTimeArgumentFactory.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.sql.Types;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.Argument;
+import org.skife.jdbi.v2.tweak.ArgumentFactory;
+
+public class DateTimeArgumentFactory implements ArgumentFactory<DateTime> {
+
+ @Override
+ public boolean accepts(final Class<?> expectedType, final Object value, final StatementContext ctx) {
+ return value instanceof DateTime;
+ }
+
+ @Override
+ public Argument build(final Class<?> expectedType, final DateTime value, final StatementContext ctx) {
+ return new DateTimeArgument(value);
+ }
+
+ public static class DateTimeArgument implements Argument {
+
+ private final DateTime value;
+
+ public DateTimeArgument(final DateTime value) {
+ this.value = value;
+ }
+
+ @Override
+ public void apply(final int position, final PreparedStatement statement, final StatementContext ctx) throws SQLException {
+ if (value != null) {
+ statement.setTimestamp(position, new Timestamp(value.toDate().getTime()));
+ } else {
+ statement.setNull(position, Types.TIMESTAMP);
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DateTimeArgument");
+ sb.append("{value=").append(value);
+ sb.append('}');
+ return sb.toString();
+ }
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/DateTimeZoneArgumentFactory.java b/util/src/main/java/org/killbill/billing/util/dao/DateTimeZoneArgumentFactory.java
new file mode 100644
index 0000000..f8fb52f
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/DateTimeZoneArgumentFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+
+import org.joda.time.DateTimeZone;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.Argument;
+import org.skife.jdbi.v2.tweak.ArgumentFactory;
+
+public class DateTimeZoneArgumentFactory implements ArgumentFactory<DateTimeZone> {
+
+ @Override
+ public boolean accepts(final Class<?> expectedType, final Object value, final StatementContext ctx) {
+ return value instanceof DateTimeZone;
+ }
+
+ @Override
+ public Argument build(final Class<?> expectedType, final DateTimeZone value, final StatementContext ctx) {
+ return new DateTimeZoneArgument(value);
+ }
+
+ public class DateTimeZoneArgument implements Argument {
+
+ private final DateTimeZone value;
+
+ public DateTimeZoneArgument(final DateTimeZone value) {
+ this.value = value;
+ }
+
+ @Override
+ public void apply(final int position, final PreparedStatement statement, final StatementContext ctx) throws SQLException {
+ if (value != null) {
+ statement.setString(position, value.toString());
+ } else {
+ statement.setNull(position, Types.VARCHAR);
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DateTimeZoneArgument");
+ sb.append("{value=").append(value);
+ sb.append('}');
+ return sb.toString();
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/DefaultNonEntityDao.java b/util/src/main/java/org/killbill/billing/util/dao/DefaultNonEntityDao.java
new file mode 100644
index 0000000..785ef8b
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/DefaultNonEntityDao.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.cache.CacheLoaderArgument;
+
+public class DefaultNonEntityDao implements NonEntityDao {
+
+ private final NonEntitySqlDao nonEntitySqlDao;
+ private final WithCaching containedCall;
+
+
+ @Inject
+ public DefaultNonEntityDao(final IDBI dbi) {
+ this.nonEntitySqlDao = dbi.onDemand(NonEntitySqlDao.class);
+ this.containedCall = new WithCaching();
+ }
+
+
+ public Long retrieveRecordIdFromObject(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+
+ return containedCall.withCaching(new OperationRetrieval<Long>() {
+ @Override
+ public Long doRetrieve(final UUID objectId, final ObjectType objectType) {
+ final TableName tableName = TableName.fromObjectType(objectType);
+ return nonEntitySqlDao.getRecordIdFromObject(objectId.toString(), tableName.getTableName());
+ }
+ }, objectId, objectType, cache);
+
+ }
+
+ public Long retrieveAccountRecordIdFromObject(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+ return containedCall.withCaching(new OperationRetrieval<Long>() {
+ @Override
+ public Long doRetrieve(final UUID objectId, final ObjectType objectType) {
+ final TableName tableName = TableName.fromObjectType(objectType);
+ switch (tableName) {
+ case TENANT:
+ case TAG_DEFINITIONS:
+ case TAG_DEFINITION_HISTORY:
+ return null;
+
+ case ACCOUNT:
+ return nonEntitySqlDao.getAccountRecordIdFromAccount(objectId.toString());
+
+ default:
+ return nonEntitySqlDao.getAccountRecordIdFromObjectOtherThanAccount(objectId.toString(), tableName.getTableName());
+ }
+ }
+ }, objectId, objectType, cache);
+ }
+
+ public Long retrieveTenantRecordIdFromObject(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+
+
+ return containedCall.withCaching(new OperationRetrieval<Long>() {
+ @Override
+ public Long doRetrieve(final UUID objectId, final ObjectType objectType) {
+ final TableName tableName = TableName.fromObjectType(objectType);
+ switch (tableName) {
+ case TENANT:
+ return nonEntitySqlDao.getTenantRecordIdFromTenant(objectId.toString());
+
+ default:
+ return nonEntitySqlDao.getTenantRecordIdFromObjectOtherThanTenant(objectId.toString(), tableName.getTableName());
+ }
+
+ }
+ }, objectId, objectType, cache);
+ }
+
+ @Override
+ public Long retrieveLastHistoryRecordIdFromTransaction(@Nullable final Long targetRecordId, final TableName tableName, final NonEntitySqlDao transactional) {
+ // There is no caching here because the value returned changes as we add more history records, and so we would need some cache invalidation
+ return transactional.getLastHistoryRecordId(targetRecordId, tableName.getTableName());
+ }
+
+ @Override
+ public Long retrieveHistoryTargetRecordId(@Nullable final Long recordId, final TableName tableName) {
+ return nonEntitySqlDao.getHistoryTargetRecordId(recordId, tableName.getTableName());
+ }
+
+ @Override
+ public UUID retrieveIdFromObject(final Long recordId, final ObjectType objectType) {
+ final TableName tableName = TableName.fromObjectType(objectType);
+ return nonEntitySqlDao.getIdFromObject(recordId, tableName.getTableName());
+ }
+
+ private interface OperationRetrieval<T> {
+ public T doRetrieve(final UUID objectId, final ObjectType objectType);
+ }
+
+ // 'cache' will be null for the CacheLoader classes -- or if cache is not configured.
+ private class WithCaching {
+
+ private Long withCaching(final OperationRetrieval<Long> op, @Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+ if (objectId == null) {
+ return null;
+ }
+
+ if (cache != null) {
+ return (Long) cache.get(objectId.toString(), new CacheLoaderArgument(objectType));
+ }
+ return op.doRetrieve(objectId, objectType);
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/EntityAudit.java b/util/src/main/java/org/killbill/billing/util/dao/EntityAudit.java
new file mode 100644
index 0000000..0cd4568
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/EntityAudit.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.entity.EntityBase;
+
+public class EntityAudit extends EntityBase {
+
+ private final TableName tableName;
+ private final Long targetRecordId;
+ private final ChangeType changeType;
+
+ public EntityAudit(final UUID entityId, final TableName tableName, final Long targetRecordId, final ChangeType changeType, final DateTime createdDate) {
+ super(entityId, createdDate, null);
+ this.tableName = tableName;
+ this.targetRecordId = targetRecordId;
+ this.changeType = changeType;
+
+ }
+ public EntityAudit(final TableName tableName, final Long targetRecordId, final ChangeType changeType, final DateTime createdDate) {
+ this(UUID.randomUUID(), tableName, targetRecordId, changeType, createdDate);
+ }
+
+ public TableName getTableName() {
+ return tableName;
+ }
+
+ public Long getTargetRecordId() {
+ return targetRecordId;
+ }
+
+ public ChangeType getChangeType() {
+ return changeType;
+ }
+
+ @Override
+ public String toString() {
+ return "EntityAudit{" +
+ "tableName=" + tableName +
+ ", targetRecordId=" + targetRecordId +
+ ", changeType=" + changeType +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final EntityAudit that = (EntityAudit) o;
+
+ if (changeType != that.changeType) {
+ return false;
+ }
+ if (tableName != that.tableName) {
+ return false;
+ }
+ if (targetRecordId != null ? !targetRecordId.equals(that.targetRecordId) : that.targetRecordId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (tableName != null ? tableName.hashCode() : 0);
+ result = 31 * result + (targetRecordId != null ? targetRecordId.hashCode() : 0);
+ result = 31 * result + (changeType != null ? changeType.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryBinder.java b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryBinder.java
new file mode 100644
index 0000000..e5afcf6
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryBinder.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.InvocationTargetException;
+
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+@BindingAnnotation(EntityHistoryBinder.EntityHistoryBinderFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface EntityHistoryBinder {
+
+ public static class EntityHistoryBinderFactory<M extends EntityModelDao<E>, E extends Entity> implements BinderFactory {
+
+ private static final Logger logger = LoggerFactory.getLogger(EntityHistoryBinder.class);
+
+ @Override
+ public Binder build(final Annotation annotation) {
+ return new Binder<EntityHistoryBinder, EntityHistoryModelDao<M, E>>() {
+
+ @Override
+ public void bind(final SQLStatement<?> q, final EntityHistoryBinder bind, final EntityHistoryModelDao<M, E> history) {
+ try {
+ // Emulate @BindBean
+ final M arg = history.getEntity();
+ final BeanInfo infos = Introspector.getBeanInfo(arg.getClass());
+ final PropertyDescriptor[] props = infos.getPropertyDescriptors();
+ for (final PropertyDescriptor prop : props) {
+ q.bind(prop.getName(), prop.getReadMethod().invoke(arg));
+ }
+ q.bind("id", history.getId());
+ q.bind("targetRecordId", history.getTargetRecordId());
+ q.bind("changeType", history.getChangeType().toString());
+ } catch (IntrospectionException e) {
+ logger.warn(e.getMessage());
+ } catch (InvocationTargetException e) {
+ logger.warn(e.getMessage());
+ } catch (IllegalAccessException e) {
+ logger.warn(e.getMessage());
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDao.java b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDao.java
new file mode 100644
index 0000000..6269944
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDao.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public class EntityHistoryModelDao<M extends EntityModelDao<E>, E extends Entity> extends EntityBase {
+
+ private Long targetRecordId;
+ private M entity;
+ private ChangeType changeType;
+
+ public EntityHistoryModelDao(final UUID id, final M src, final Long targetRecordId, final ChangeType type, final DateTime createdDate) {
+ super(id, createdDate, createdDate);
+ this.changeType = type;
+ this.targetRecordId = targetRecordId;
+ this.entity = src;
+ }
+
+ public EntityHistoryModelDao(final M src, final Long targetRecordId, final ChangeType type, final DateTime createdDate) {
+ this(UUID.randomUUID(), src, targetRecordId, type, createdDate);
+ }
+
+ public ChangeType getChangeType() {
+ return changeType;
+ }
+
+ public M getEntity() {
+ return entity;
+ }
+
+ public Long getTargetRecordId() {
+ return targetRecordId;
+ }
+
+ public void setTargetRecordId(final Long targetRecordId) {
+ this.targetRecordId = targetRecordId;
+ }
+
+ public void setEntity(final M entity) {
+ this.entity = entity;
+ }
+
+ public void setChangeType(final ChangeType changeType) {
+ this.changeType = changeType;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/EnumArgumentFactory.java b/util/src/main/java/org/killbill/billing/util/dao/EnumArgumentFactory.java
new file mode 100644
index 0000000..aad0962
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/EnumArgumentFactory.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.Argument;
+import org.skife.jdbi.v2.tweak.ArgumentFactory;
+
+public class EnumArgumentFactory implements ArgumentFactory<Enum> {
+
+ @Override
+ public Argument build(final Class<?> expectedType, final Enum value, final StatementContext ctx) {
+ return new StringArgument(value.toString());
+ }
+
+ class StringArgument implements Argument {
+
+ private final String value;
+
+ StringArgument(final String value) {
+ this.value = value;
+ }
+
+ public void apply(final int position, final PreparedStatement statement, final StatementContext ctx) throws SQLException {
+ if (value != null) {
+ statement.setString(position, value);
+ } else {
+ statement.setNull(position, Types.VARCHAR);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "'" + value + "'";
+ }
+ }
+
+ @Override
+ public boolean accepts(final Class expectedType, final Object value, final StatementContext ctx) {
+ return value != null && (value instanceof Enum /* Works for Enum inside classes */ || value.getClass().isEnum());
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java b/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java
new file mode 100644
index 0000000..c829280
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+
+public interface HistorySqlDao<M extends EntityModelDao<E>, E extends Entity> {
+
+ @SqlUpdate
+ public void addHistoryFromTransaction(@EntityHistoryBinder EntityHistoryModelDao<M, E> history,
+ @BindBean InternalCallContext context);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/LocalDateArgumentFactory.java b/util/src/main/java/org/killbill/billing/util/dao/LocalDateArgumentFactory.java
new file mode 100644
index 0000000..4bbc8ca
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/LocalDateArgumentFactory.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+
+import org.joda.time.LocalDate;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.Argument;
+import org.skife.jdbi.v2.tweak.ArgumentFactory;
+
+public class LocalDateArgumentFactory implements ArgumentFactory<LocalDate> {
+
+ @Override
+ public boolean accepts(final Class<?> expectedType, final Object value, final StatementContext ctx) {
+ return value instanceof LocalDate;
+ }
+
+ @Override
+ public Argument build(final Class<?> expectedType, final LocalDate value, final StatementContext ctx) {
+ return new LocalDateArgument(value);
+ }
+
+ public static class LocalDateArgument implements Argument {
+
+ private final LocalDate value;
+
+ public LocalDateArgument(final LocalDate value) {
+ this.value = value;
+ }
+
+ @Override
+ public void apply(final int position, final PreparedStatement statement, final StatementContext ctx) throws SQLException {
+ if (value != null) {
+ // ISO8601 format
+ statement.setString(position, value.toString());
+ } else {
+ statement.setNull(position, Types.VARCHAR);
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("LocalDateArgument");
+ sb.append("{value=").append(value);
+ sb.append('}');
+ return sb.toString();
+ }
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/LowerToCamelBeanMapper.java b/util/src/main/java/org/killbill/billing/util/dao/LowerToCamelBeanMapper.java
new file mode 100644
index 0000000..6616de8
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/LowerToCamelBeanMapper.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.sql.Date;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import com.google.common.base.CaseFormat;
+
+// Identical to org.skife.jdbi.v2.BeanMapper but maps created_date to createdDate
+public class LowerToCamelBeanMapper<T> implements ResultSetMapper<T> {
+
+ private final Class<T> type;
+ private final Map<String, PropertyDescriptor> properties = new HashMap<String, PropertyDescriptor>();
+
+ public LowerToCamelBeanMapper(final Class<T> type) {
+ this.type = type;
+ try {
+ final BeanInfo info = Introspector.getBeanInfo(type);
+
+ for (final PropertyDescriptor descriptor : info.getPropertyDescriptors()) {
+ properties.put(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, descriptor.getName()).toLowerCase(), descriptor);
+ }
+ } catch (IntrospectionException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public T map(final int row, final ResultSet rs, final StatementContext ctx) throws SQLException {
+ final T bean;
+ try {
+ bean = type.newInstance();
+ } catch (Exception e) {
+ throw new IllegalArgumentException(String.format("A bean, %s, was mapped " +
+ "which was not instantiable", type.getName()),
+ e);
+ }
+
+ final Class beanClass = bean.getClass();
+ final ResultSetMetaData metadata = rs.getMetaData();
+
+ for (int i = 1; i <= metadata.getColumnCount(); ++i) {
+ final String name = metadata.getColumnLabel(i).toLowerCase();
+
+ final PropertyDescriptor descriptor = properties.get(name);
+
+ if (descriptor != null) {
+ final Class<?> type = descriptor.getPropertyType();
+
+ Object value;
+
+ if (type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class)) {
+ value = rs.getBoolean(i);
+ } else if (type.isAssignableFrom(Byte.class) || type.isAssignableFrom(byte.class)) {
+ value = rs.getByte(i);
+ } else if (type.isAssignableFrom(Short.class) || type.isAssignableFrom(short.class)) {
+ value = rs.getShort(i);
+ } else if (type.isAssignableFrom(Integer.class) || type.isAssignableFrom(int.class)) {
+ value = rs.getInt(i);
+ } else if (type.isAssignableFrom(Long.class) || type.isAssignableFrom(long.class)) {
+ value = rs.getLong(i);
+ } else if (type.isAssignableFrom(Float.class) || type.isAssignableFrom(float.class)) {
+ value = rs.getFloat(i);
+ } else if (type.isAssignableFrom(Double.class) || type.isAssignableFrom(double.class)) {
+ value = rs.getDouble(i);
+ } else if (type.isAssignableFrom(BigDecimal.class)) {
+ value = rs.getBigDecimal(i);
+ } else if (type.isAssignableFrom(DateTime.class)) {
+ final Timestamp timestamp = rs.getTimestamp(i);
+ value = timestamp == null ? null : new DateTime(timestamp).toDateTime(DateTimeZone.UTC);
+ } else if (type.isAssignableFrom(Time.class)) {
+ value = rs.getTime(i);
+ } else if (type.isAssignableFrom(LocalDate.class)) {
+ final Date date = rs.getDate(i);
+ value = date == null ? null : new LocalDate(date, DateTimeZone.UTC);
+ } else if (type.isAssignableFrom(DateTimeZone.class)) {
+ final String dateTimeZoneString = rs.getString(i);
+ value = dateTimeZoneString == null ? null : DateTimeZone.forID(dateTimeZoneString);
+ } else if (type.isAssignableFrom(String.class)) {
+ value = rs.getString(i);
+ } else if (type.isAssignableFrom(UUID.class)) {
+ final String uuidString = rs.getString(i);
+ value = uuidString == null ? null : UUID.fromString(uuidString);
+ } else if (type.isEnum()) {
+ final String enumString = rs.getString(i);
+ //noinspection unchecked
+ value = enumString == null ? null : Enum.valueOf((Class<Enum>) type, enumString);
+ } else {
+ value = rs.getObject(i);
+ }
+
+ if (rs.wasNull() && !type.isPrimitive()) {
+ value = null;
+ }
+
+ try {
+ final Method writeMethod = descriptor.getWriteMethod();
+ if (writeMethod != null) {
+ writeMethod.invoke(bean, value);
+ } else {
+ final String camelCasedName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name);
+ final Field field = getField(beanClass, camelCasedName);
+ field.setAccessible(true); // Often private...
+ field.set(bean, value);
+ }
+ } catch (NoSuchFieldException e) {
+ throw new IllegalArgumentException(String.format("Unable to find field for " +
+ "property, %s", name), e);
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(String.format("Unable to access setter for " +
+ "property, %s", name), e);
+ } catch (InvocationTargetException e) {
+ throw new IllegalArgumentException(String.format("Invocation target exception trying to " +
+ "invoker setter for the %s property", name), e);
+ } catch (NullPointerException e) {
+ throw new IllegalArgumentException(String.format("No appropriate method to " +
+ "write value %s ", value.toString()), e);
+ }
+ }
+ }
+
+ return bean;
+ }
+
+ private static Field getField(final Class clazz, final String fieldName) throws NoSuchFieldException {
+ try {
+ return clazz.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ // Go up in the hierarchy
+ final Class superClass = clazz.getSuperclass();
+ if (superClass == null) {
+ throw e;
+ } else {
+ return getField(superClass, fieldName);
+ }
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/LowerToCamelBeanMapperFactory.java b/util/src/main/java/org/killbill/billing/util/dao/LowerToCamelBeanMapperFactory.java
new file mode 100644
index 0000000..f0175ae
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/LowerToCamelBeanMapperFactory.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import org.skife.jdbi.v2.ResultSetMapperFactory;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import org.killbill.billing.util.entity.Entity;
+
+public class LowerToCamelBeanMapperFactory implements ResultSetMapperFactory {
+
+ private final Class<? extends Entity> modelClazz;
+
+ public LowerToCamelBeanMapperFactory(final Class<? extends Entity> modelClazz) {
+ this.modelClazz = modelClazz;
+ }
+
+ @Override
+ public boolean accepts(final Class type, final StatementContext ctx) {
+ return type.equals(modelClazz);
+ }
+
+ @Override
+ public ResultSetMapper mapperFor(final Class type, final StatementContext ctx) {
+ return new LowerToCamelBeanMapper(type);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/Mapper.java b/util/src/main/java/org/killbill/billing/util/dao/Mapper.java
new file mode 100644
index 0000000..000ec16
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/Mapper.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+public class Mapper<K, V> {
+ private final K key;
+ private final V value;
+
+ public Mapper(final K key, final V value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public K getKey() {
+ return key;
+ }
+
+ public V getValue() {
+ return value;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/MapperBase.java b/util/src/main/java/org/killbill/billing/util/dao/MapperBase.java
new file mode 100644
index 0000000..0ff05b9
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/MapperBase.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.Date;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+
+public abstract class MapperBase {
+ protected LocalDate getDate(final ResultSet rs, final String fieldName) throws SQLException {
+ final Date resultStamp = rs.getDate(fieldName);
+ return rs.wasNull() ? null : new LocalDate(resultStamp, DateTimeZone.UTC);
+ }
+
+ protected DateTime getDateTime(final ResultSet rs, final String fieldName) throws SQLException {
+ final Timestamp resultStamp = rs.getTimestamp(fieldName);
+ return rs.wasNull() ? null : new DateTime(resultStamp).toDateTime(DateTimeZone.UTC);
+ }
+
+ protected UUID getUUID(final ResultSet resultSet, final String fieldName) throws SQLException {
+ final String result = resultSet.getString(fieldName);
+ return result == null ? null : UUID.fromString(result);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/NonEntityDao.java b/util/src/main/java/org/killbill/billing/util/dao/NonEntityDao.java
new file mode 100644
index 0000000..dcfee7a
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/NonEntityDao.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+
+public interface NonEntityDao {
+
+ //
+ // TODO should we check for InternalCallContext?
+ // That seems difficult because those APIs are called when creating a callcontext or from the cache loaders which also dpn't know anything about callcontext
+ //
+ public Long retrieveRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache);
+
+ public Long retrieveAccountRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache);
+
+ public Long retrieveTenantRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache);
+
+ // This retrieves from the history table the latest record for which targetId matches the one we are passing
+ public Long retrieveLastHistoryRecordIdFromTransaction(final Long targetRecordId, final TableName tableName, final NonEntitySqlDao transactional);
+
+ // This is the reverse from retrieveLastHistoryRecordIdFromTransaction; this retrieves the record_id of the object matching a given history row
+ public Long retrieveHistoryTargetRecordId(final Long recordId, final TableName tableName);
+
+ public UUID retrieveIdFromObject(final Long recordId, final ObjectType objectType);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/NonEntitySqlDao.java b/util/src/main/java/org/killbill/billing/util/dao/NonEntitySqlDao.java
new file mode 100644
index 0000000..b944c0d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/NonEntitySqlDao.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.util.UUID;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.customizers.Define;
+import org.skife.jdbi.v2.sqlobject.mixins.CloseMe;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+@UseStringTemplate3StatementLocator
+public interface NonEntitySqlDao extends Transactional<NonEntitySqlDao>, CloseMe {
+
+ @SqlQuery
+ public Long getRecordIdFromObject(@Bind("id") String id, @Define("tableName") final String tableName);
+
+ @SqlQuery
+ public UUID getIdFromObject(@Bind("recordId") Long recordId, @Define("tableName") final String tableName);
+
+ @SqlQuery
+ public Long getAccountRecordIdFromAccount(@Bind("id") String id);
+
+ @SqlQuery
+ public Long getAccountRecordIdFromAccountHistory(@Bind("id") String id);
+
+ @SqlQuery
+ public Long getAccountRecordIdFromObjectOtherThanAccount(@Bind("id") String id, @Define("tableName") final String tableName);
+
+ @SqlQuery
+ public Long getTenantRecordIdFromTenant(@Bind("id") String id);
+
+ @SqlQuery
+ public Long getTenantRecordIdFromObjectOtherThanTenant(@Bind("id") String id, @Define("tableName") final String tableName);
+
+ @SqlQuery
+ public Long getLastHistoryRecordId(@Bind("targetRecordId") Long targetRecordId, @Define("tableName") final String tableName);
+
+ @SqlQuery
+ public Long getHistoryTargetRecordId(@Bind("recordId") Long recordId, @Define("tableName") final String tableName);
+
+ @SqlQuery
+ public Iterable<RecordIdIdMappings> getHistoryRecordIdIdMappings(@Define("tableName") String tableName,
+ @Define("historyTableName") String historyTableName,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public Iterable<RecordIdIdMappings> getHistoryRecordIdIdMappingsForAccountsTable(@Define("tableName") String tableName,
+ @Define("historyTableName") String historyTableName,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public Iterable<RecordIdIdMappings> getHistoryRecordIdIdMappingsForTablesWithoutAccountRecordId(@Define("tableName") String tableName,
+ @Define("historyTableName") String historyTableName,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public Iterable<RecordIdIdMappings> getRecordIdIdMappings(@Define("tableName") String tableName,
+ @BindBean final InternalTenantContext context);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/RecordIdIdMappings.java b/util/src/main/java/org/killbill/billing/util/dao/RecordIdIdMappings.java
new file mode 100644
index 0000000..e46c9ae
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/RecordIdIdMappings.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public class RecordIdIdMappings {
+
+ private final Long recordId;
+ private final UUID id;
+
+ public RecordIdIdMappings(final long recordId, final UUID id) {
+ this.recordId = recordId;
+ this.id = id;
+ }
+
+ public Long getRecordId() {
+ return recordId;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public static Map<Long, UUID> toMap(final Iterable<RecordIdIdMappings> mappings) {
+ final Map<Long, UUID> result = new LinkedHashMap<Long, UUID>();
+ for (final RecordIdIdMappings mapping : mappings) {
+ result.put(mapping.getRecordId(), mapping.getId());
+ }
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/RecordIdIdMappingsMapper.java b/util/src/main/java/org/killbill/billing/util/dao/RecordIdIdMappingsMapper.java
new file mode 100644
index 0000000..43c7498
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/RecordIdIdMappingsMapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Map;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import org.killbill.billing.callcontext.DefaultCallContext;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.audit.dao.AuditLogModelDao;
+import org.killbill.billing.util.callcontext.CallContext;
+
+public class RecordIdIdMappingsMapper extends MapperBase implements ResultSetMapper<RecordIdIdMappings> {
+
+ @Override
+ public RecordIdIdMappings map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+ final long recordId = r.getLong("record_id");
+ final UUID id = getUUID(r, "id");
+ return new RecordIdIdMappings(recordId, id);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/TableName.java b/util/src/main/java/org/killbill/billing/util/dao/TableName.java
new file mode 100644
index 0000000..9157f35
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/TableName.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License")), you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.ObjectType;
+
+/**
+ * Map table names to entity object types and classes, and history tables (if exists)
+ */
+public enum TableName {
+ ACCOUNT_HISTORY("account_history"),
+ ACCOUNT("accounts", ObjectType.ACCOUNT, ACCOUNT_HISTORY),
+ ACCOUNT_EMAIL_HISTORY("account_email_history"),
+ ACCOUNT_EMAIL("account_emails", ObjectType.ACCOUNT_EMAIL, ACCOUNT_EMAIL_HISTORY),
+ BUNDLES("bundles", ObjectType.BUNDLE),
+ BLOCKING_STATES("blocking_states", ObjectType.BLOCKING_STATES),
+ CUSTOM_FIELD_HISTORY("custom_field_history"),
+ CUSTOM_FIELD("custom_fields", ObjectType.CUSTOM_FIELD, CUSTOM_FIELD_HISTORY),
+ INVOICE_ITEMS("invoice_items", ObjectType.INVOICE_ITEM),
+ INVOICE_PAYMENTS("invoice_payments", ObjectType.INVOICE_PAYMENT),
+ INVOICES("invoices", ObjectType.INVOICE),
+ PAYMENT_ATTEMPT_HISTORY("payment_attempt_history"),
+ PAYMENT_ATTEMPTS("payment_attempts", ObjectType.PAYMENT_ATTEMPT, PAYMENT_ATTEMPT_HISTORY),
+ PAYMENT_HISTORY("payment_history"),
+ PAYMENTS("payments", ObjectType.PAYMENT, PAYMENT_HISTORY),
+ PAYMENT_METHOD_HISTORY("payment_method_history"),
+ PAYMENT_METHODS("payment_methods", ObjectType.PAYMENT_METHOD, PAYMENT_METHOD_HISTORY),
+ SUBSCRIPTIONS("subscriptions", ObjectType.SUBSCRIPTION),
+ SUBSCRIPTION_EVENTS("subscription_events", ObjectType.SUBSCRIPTION_EVENT),
+ REFUND_HISTORY("refund_history"),
+ REFUNDS("refunds", ObjectType.REFUND, REFUND_HISTORY),
+ TAG_DEFINITION_HISTORY("tag_definition_history"),
+ TAG_DEFINITIONS("tag_definitions", ObjectType.TAG_DEFINITION, TAG_DEFINITION_HISTORY),
+ TAG_HISTORY("tag_history"),
+ TENANT("tenants", ObjectType.TENANT),
+ TENANT_KVS("tenant_kvs", ObjectType.TENANT_KVS),
+ TAG("tags", ObjectType.TAG, TAG_HISTORY);
+
+ private final String tableName;
+ private final ObjectType objectType;
+ private final TableName historyTableName;
+
+ TableName(final String tableName, @Nullable final ObjectType objectType, @Nullable final TableName historyTableName) {
+ this.tableName = tableName;
+ this.objectType = objectType;
+ this.historyTableName = historyTableName;
+ }
+
+ TableName(final String tableName, final ObjectType objectType) {
+ this(tableName, objectType, null);
+ }
+
+ TableName(final String tableName) {
+ this(tableName, null, null);
+ }
+
+ public static TableName fromObjectType(final ObjectType objectType) {
+ for (final TableName tableName : values()) {
+ if (tableName.getObjectType() != null && tableName.getObjectType().equals(objectType)) {
+ return tableName;
+ }
+ }
+ return null;
+ }
+
+ public String getTableName() {
+ return tableName;
+ }
+
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ public TableName getHistoryTableName() {
+ return historyTableName;
+ }
+
+ public boolean hasHistoryTable() {
+ return historyTableName != null;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/UUIDArgumentFactory.java b/util/src/main/java/org/killbill/billing/util/dao/UUIDArgumentFactory.java
new file mode 100644
index 0000000..e806ea2
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/UUIDArgumentFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.Argument;
+import org.skife.jdbi.v2.tweak.ArgumentFactory;
+
+public class UUIDArgumentFactory implements ArgumentFactory<UUID> {
+
+ @Override
+ public boolean accepts(final Class<?> expectedType, final Object value, final StatementContext ctx) {
+ return value instanceof UUID;
+ }
+
+ @Override
+ public Argument build(final Class<?> expectedType, final UUID value, final StatementContext ctx) {
+ return new UUIDArgument(value);
+ }
+
+ public class UUIDArgument implements Argument {
+
+ private final UUID value;
+
+ public UUIDArgument(final UUID value) {
+ this.value = value;
+ }
+
+ @Override
+ public void apply(final int position, final PreparedStatement statement, final StatementContext ctx) throws SQLException {
+ if (value != null) {
+ statement.setString(position, value.toString());
+ } else {
+ statement.setNull(position, Types.VARCHAR);
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("UUIDArgument");
+ sb.append("{value=").append(value);
+ sb.append('}');
+ return sb.toString();
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/UuidMapper.java b/util/src/main/java/org/killbill/billing/util/dao/UuidMapper.java
new file mode 100644
index 0000000..5d6e26e
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/UuidMapper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class UuidMapper implements ResultSetMapper<UUID> {
+ @Override
+ public UUID map(final int index, final ResultSet resultSet, final StatementContext statementContext) throws SQLException {
+ return UUID.fromString(resultSet.getString(1));
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/DefaultAmountFormatter.java b/util/src/main/java/org/killbill/billing/util/DefaultAmountFormatter.java
new file mode 100644
index 0000000..60ba8f5
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/DefaultAmountFormatter.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util;
+
+import java.math.BigDecimal;
+
+public class DefaultAmountFormatter {
+
+ public static final int SCALE = 2;
+
+ // Static only
+ private DefaultAmountFormatter() {
+ }
+
+ public static BigDecimal round(final BigDecimal decimal) {
+ if (decimal == null) {
+ return BigDecimal.ZERO.setScale(SCALE, BigDecimal.ROUND_HALF_UP);
+ } else {
+ return decimal.setScale(SCALE, BigDecimal.ROUND_HALF_UP);
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/email/DefaultEmailSender.java b/util/src/main/java/org/killbill/billing/util/email/DefaultEmailSender.java
new file mode 100644
index 0000000..7c5c8f2
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/email/DefaultEmailSender.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.email;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.commons.mail.Email;
+import org.apache.commons.mail.EmailException;
+import org.apache.commons.mail.HtmlEmail;
+import org.apache.commons.mail.SimpleEmail;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ErrorCode;
+
+import com.google.inject.Inject;
+
+public class DefaultEmailSender implements EmailSender {
+
+ private final Logger log = LoggerFactory.getLogger(EmailSender.class);
+ private final EmailConfig config;
+
+ @Inject
+ public DefaultEmailSender(final EmailConfig emailConfig) {
+ this.config = emailConfig;
+ }
+
+ @Override
+ public void sendHTMLEmail(final List<String> to, final List<String> cc, final String subject, final String htmlBody) throws EmailApiException {
+ final HtmlEmail email = new HtmlEmail();
+ try {
+ email.setHtmlMsg(htmlBody);
+ } catch (EmailException e) {
+ throw new EmailApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
+ }
+
+ sendEmail(to, cc, subject, email);
+ }
+
+ @Override
+ public void sendPlainTextEmail(final List<String> to, final List<String> cc, final String subject, final String body) throws IOException, EmailApiException {
+ final SimpleEmail email = new SimpleEmail();
+ try {
+ email.setMsg(body);
+ } catch (EmailException e) {
+ throw new EmailApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
+ }
+
+ sendEmail(to, cc, subject, email);
+ }
+
+ private void sendEmail(final List<String> to, final List<String> cc, final String subject, final Email email) throws EmailApiException {
+ try {
+ email.setSmtpPort(config.getSmtpPort());
+ if (config.useSmtpAuth()) {
+ email.setAuthentication(config.getSmtpUserName(), config.getSmtpPassword());
+ }
+ email.setHostName(config.getSmtpServerName());
+ email.setFrom(config.getDefaultFrom());
+
+ email.setSubject(subject);
+
+ if (to != null) {
+ for (final String recipient : to) {
+ email.addTo(recipient);
+ }
+ }
+
+ if (cc != null) {
+ for (final String recipient : cc) {
+ email.addCc(recipient);
+ }
+ }
+
+ email.setSSL(config.useSSL());
+
+ log.info("Sending email to {}, cc {}, subject {}", new Object[]{to, cc, subject});
+ email.send();
+ } catch (EmailException ee) {
+ throw new EmailApiException(ee, ErrorCode.EMAIL_SENDING_FAILED);
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/email/EmailConfig.java b/util/src/main/java/org/killbill/billing/util/email/EmailConfig.java
new file mode 100644
index 0000000..edcfdea
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/email/EmailConfig.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.email;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.DefaultNull;
+import org.skife.config.Description;
+
+import org.killbill.billing.util.config.KillbillConfig;
+
+public interface EmailConfig extends KillbillConfig {
+
+ @Config("org.killbill.mail.smtp.host")
+ @DefaultNull
+ @Description("MTA host used for email notifications")
+ public String getSmtpServerName();
+
+ @Config("org.killbill.mail.smtp.port")
+ @DefaultNull
+ @Description("MTA port used for email notifications")
+ public int getSmtpPort();
+
+ @Config("org.killbill.mail.smtp.auth")
+ @Default("false")
+ @Description("Whether to authenticate against the MTA")
+ public boolean useSmtpAuth();
+
+ @Config("org.killbill.mail.smtp.user")
+ @DefaultNull
+ @Description("Username to use to authenticate against the MTA")
+ public String getSmtpUserName();
+
+ @Config("org.killbill.mail.smtp.password")
+ @DefaultNull
+ @Description("Password to use to authenticate against the MTA")
+ public String getSmtpPassword();
+
+ @Config("org.killbill.mail.from")
+ @Default("support@example.com")
+ @Description("Default From: field for email notifications")
+ String getDefaultFrom();
+
+ @Config("org.killbill.mail.useSSL")
+ @Default("false")
+ @Description("Whether to use secure SMTP")
+ boolean useSSL();
+
+ @Config("org.killbill.mail.invoiceEmailSubject")
+ @Default("Your invoice")
+ @Description("Default Subject: field for invoice notifications")
+ String getInvoiceEmailSubject();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/email/EmailModule.java b/util/src/main/java/org/killbill/billing/util/email/EmailModule.java
new file mode 100644
index 0000000..60facd3
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/email/EmailModule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.email;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import com.google.inject.AbstractModule;
+
+public class EmailModule extends AbstractModule {
+
+ protected final ConfigSource configSource;
+
+ public EmailModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ protected void installEmailConfig() {
+ final EmailConfig config = new ConfigurationObjectFactory(configSource).build(EmailConfig.class);
+ bind(EmailConfig.class).toInstance(config);
+ }
+
+ @Override
+ protected void configure() {
+ installEmailConfig();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java b/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java
new file mode 100644
index 0000000..3f785e5
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.email.templates;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.util.Map;
+
+import org.killbill.billing.util.config.catalog.UriAccessor;
+import org.killbill.billing.util.io.IOUtils;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.samskivert.mustache.Mustache;
+import com.samskivert.mustache.Template;
+
+public class MustacheTemplateEngine implements TemplateEngine {
+
+ @Override
+ public String executeTemplate(final String templateName, final Map<String, Object> data) throws IOException {
+ final String templateText = getTemplateText(templateName);
+ return executeTemplateText(templateText, data);
+ }
+
+ @VisibleForTesting
+ public String executeTemplateText(final String templateText, final Map<String, Object> data) {
+ final Template template = Mustache.compiler().compile(templateText);
+ return template.execute(data);
+ }
+
+ private String getTemplateText(final String templateName) throws IOException {
+ final InputStream templateStream;
+ try {
+ templateStream = UriAccessor.accessUri(templateName);
+ } catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+
+ return IOUtils.toString(templateStream);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/email/templates/TemplateEngine.java b/util/src/main/java/org/killbill/billing/util/email/templates/TemplateEngine.java
new file mode 100644
index 0000000..2854b39
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/email/templates/TemplateEngine.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.email.templates;
+
+import java.io.IOException;
+import java.util.Map;
+
+public interface TemplateEngine {
+ public String executeTemplate(String templateName, Map<String, Object> data) throws IOException;
+}
diff --git a/util/src/main/java/org/killbill/billing/util/email/templates/TemplateModule.java b/util/src/main/java/org/killbill/billing/util/email/templates/TemplateModule.java
new file mode 100644
index 0000000..96c40d3
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/email/templates/TemplateModule.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.email.templates;
+
+import com.google.inject.AbstractModule;
+
+public class TemplateModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(TemplateEngine.class).to(MustacheTemplateEngine.class).asEagerSingleton();
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/Audited.java b/util/src/main/java/org/killbill/billing/util/entity/dao/Audited.java
new file mode 100644
index 0000000..9e7f3cb
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/Audited.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.killbill.billing.util.audit.ChangeType;
+
+/**
+ * The <code>Audited</code> annotation wraps a Sql dao method and
+ * create Audit and History entries as needed. Every r/w
+ * database operation on any Entity should have this annotation.
+ * <p/>
+ * To create a audit entries automatically for some method <code>updateChargedThroughDate</code>:
+ * <pre>
+ * @Audited(type = ChangeType.UPDATE)
+ * @SqlUpdate public void updateChargedThroughDate(@Bind("id") String id,
+ * @Bind("chargedThroughDate") Date chargedThroughDate,
+ * @InternalTenantContextBinder final InternalCallContext callcontext);
+ * </pre>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface Audited {
+
+ /**
+ * @return the type of operation
+ */
+ ChangeType value();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationHelper.java b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationHelper.java
new file mode 100644
index 0000000..6b0c800
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationHelper.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.util.customfield.ShouldntHappenException;
+import org.killbill.billing.util.entity.DefaultPagination;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+
+public class DefaultPaginationHelper {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultPaginationHelper.class);
+
+ public abstract static class EntityPaginationBuilder<E extends Entity, T extends BillingExceptionBase> {
+
+ public abstract Pagination<E> build(final Long offset, final Long limit, final String pluginName) throws T;
+ }
+
+ public static <E extends Entity, T extends BillingExceptionBase> Pagination<E> getEntityPaginationFromPlugins(final Iterable<String> plugins, final Long offset, final Long limit, final EntityPaginationBuilder<E, T> entityPaginationBuilder) {
+ // Note that we cannot easily do streaming here, since we would have to rely on the statistics
+ // returned by the Pagination objects from the plugins and we probably don't want to do that (if
+ // one plugin gets it wrong, it may starve the others).
+ final List<E> allResults = new LinkedList<E>();
+ Long totalNbRecords = 0L;
+ Long maxNbRecords = 0L;
+
+ // Search in all plugins (we treat the full set of results as a union with respect to offset/limit)
+ boolean firstSearch = true;
+ for (final String pluginName : plugins) {
+ try {
+ final Pagination<E> pages;
+ if (allResults.size() >= limit) {
+ // We have enough results, we just keep going (limit 1) to get the stats
+ pages = entityPaginationBuilder.build(firstSearch ? offset : 0L, 1L, pluginName);
+ // Required to close database connections
+ ImmutableList.<E>copyOf(pages);
+ } else {
+ pages = entityPaginationBuilder.build(firstSearch ? offset : 0L, limit - allResults.size(), pluginName);
+ allResults.addAll(ImmutableList.<E>copyOf(pages));
+ }
+ // Make sure not to start at 0 for subsequent plugins if previous ones didn't yield any result
+ firstSearch = allResults.isEmpty();
+ totalNbRecords += pages.getTotalNbRecords();
+ maxNbRecords += pages.getMaxNbRecords();
+ } catch (final BillingExceptionBase e) {
+ log.warn("Error while searching plugin " + pluginName, e);
+ // Non-fatal, continue to search other plugins
+ }
+ }
+
+ return new DefaultPagination<E>(offset, limit, totalNbRecords, maxNbRecords, allResults.iterator());
+ }
+
+ public abstract static class SourcePaginationBuilder<O, T extends BillingExceptionBase> {
+
+ public abstract Pagination<O> build() throws T;
+ }
+
+ public static <E extends Entity, O, T extends BillingExceptionBase> Pagination<E> getEntityPagination(final Long limit,
+ final SourcePaginationBuilder<O, T> sourcePaginationBuilder,
+ final Function<O, E> function) throws T {
+ final Pagination<O> modelsDao = sourcePaginationBuilder.build();
+
+ return new DefaultPagination<E>(modelsDao,
+ limit,
+ Iterators.<E>filter(Iterators.<O, E>transform(modelsDao.iterator(), function),
+ Predicates.<E>notNull()));
+ }
+
+ public static <E extends Entity, O, T extends BillingExceptionBase> Pagination<E> getEntityPaginationNoException(final Long limit,
+ final SourcePaginationBuilder<O, T> sourcePaginationBuilder,
+ final Function<O, E> function) {
+ try {
+ return getEntityPagination(limit, sourcePaginationBuilder, function);
+ } catch (final BillingExceptionBase e) {
+ throw new ShouldntHappenException("No exception expected" + e);
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
new file mode 100644
index 0000000..38c8d91
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import java.util.Iterator;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.DefaultPagination;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.Pagination;
+
+public class DefaultPaginationSqlDaoHelper {
+
+ private final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
+
+ public DefaultPaginationSqlDaoHelper(final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao) {
+ this.transactionalSqlDao = transactionalSqlDao;
+ }
+
+ public <E extends Entity, M extends EntityModelDao<E>, S extends EntitySqlDao<M, E>> Pagination<M> getPagination(final Class<? extends EntitySqlDao<M, E>> sqlDaoClazz,
+ final PaginationIteratorBuilder<M, E, S> paginationIteratorBuilder,
+ final Long offset,
+ final Long limit,
+ final InternalTenantContext context) {
+ // Note: the connection will be busy as we stream the results out: hence we cannot use
+ // SQL_CALC_FOUND_ROWS / FOUND_ROWS on the actual query.
+ // We still need to know the actual number of results, mainly for the UI so that it knows if it needs to fetch
+ // more pages.
+ final Long count = transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Long>() {
+ @Override
+ public Long inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final EntitySqlDao<M, E> sqlDao = entitySqlDaoWrapperFactory.become(sqlDaoClazz);
+ return paginationIteratorBuilder.getCount((S) sqlDao, context);
+ }
+ });
+
+ // We usually always want to wrap our queries in an EntitySqlDaoTransactionWrapper... except here.
+ // Since we want to stream the results out, we don't want to auto-commit when this method returns.
+ final EntitySqlDao<M, E> sqlDao = transactionalSqlDao.onDemand(sqlDaoClazz);
+ final Long totalCount = sqlDao.getCount(context);
+ final Iterator<M> results = paginationIteratorBuilder.build((S) sqlDao, limit, context);
+
+ return new DefaultPagination<M>(offset, limit, count, totalCount, results);
+ }
+
+ public abstract static class PaginationIteratorBuilder<M extends EntityModelDao<E>, E extends Entity, S extends EntitySqlDao<M, E>> {
+
+ public abstract Long getCount(final S sqlDao, final InternalTenantContext context);
+
+ public abstract Iterator<M> build(final S sqlDao, final Long limit, final InternalTenantContext context);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntityDao.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntityDao.java
new file mode 100644
index 0000000..953f1fc
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntityDao.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import java.util.UUID;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.Pagination;
+
+public interface EntityDao<M extends EntityModelDao<E>, E extends Entity, U extends BillingExceptionBase> {
+
+ public void create(M entity, InternalCallContext context) throws U;
+
+ public Long getRecordId(UUID id, InternalTenantContext context);
+
+ public M getByRecordId(Long recordId, InternalTenantContext context);
+
+ public M getById(UUID id, InternalTenantContext context);
+
+ public Pagination<M> getAll(InternalTenantContext context);
+
+ public Pagination<M> get(Long offset, Long limit, InternalTenantContext context);
+
+ public Long getCount(InternalTenantContext context);
+
+ public void test(InternalTenantContext context);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntityDaoBase.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntityDaoBase.java
new file mode 100644
index 0000000..0704baa
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntityDaoBase.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import java.util.Iterator;
+import java.util.UUID;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.entity.DefaultPagination;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
+
+public abstract class EntityDaoBase<M extends EntityModelDao<E>, E extends Entity, U extends BillingExceptionBase> implements EntityDao<M, E, U> {
+
+ protected final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
+ protected final DefaultPaginationSqlDaoHelper paginationHelper;
+
+ private final Class<? extends EntitySqlDao<M, E>> realSqlDao;
+
+ public EntityDaoBase(final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao, final Class<? extends EntitySqlDao<M, E>> realSqlDao) {
+ this.transactionalSqlDao = transactionalSqlDao;
+ this.realSqlDao = realSqlDao;
+ this.paginationHelper = new DefaultPaginationSqlDaoHelper(transactionalSqlDao);
+ }
+
+ @Override
+ public void create(final M entity, final InternalCallContext context) throws U {
+ transactionalSqlDao.execute(getCreateEntitySqlDaoTransactionWrapper(entity, context));
+ }
+
+ protected EntitySqlDaoTransactionWrapper<Void> getCreateEntitySqlDaoTransactionWrapper(final M entity, final InternalCallContext context) {
+ return new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final EntitySqlDao<M, E> transactional = entitySqlDaoWrapperFactory.become(realSqlDao);
+
+ if (checkEntityAlreadyExists(transactional, entity, context)) {
+ throw generateAlreadyExistsException(entity, context);
+ }
+ transactional.create(entity, context);
+
+ final M refreshedEntity = transactional.getById(entity.getId().toString(), context);
+
+ postBusEventFromTransaction(entity, refreshedEntity, ChangeType.INSERT, entitySqlDaoWrapperFactory, context);
+ return null;
+ }
+ };
+ }
+
+ protected boolean checkEntityAlreadyExists(final EntitySqlDao<M, E> transactional, final M entity, final InternalCallContext context) {
+ return transactional.getById(entity.getId().toString(), context) != null;
+ }
+
+ protected void postBusEventFromTransaction(final M entity, final M savedEntity, final ChangeType changeType,
+ final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory,
+ final InternalCallContext context) throws BillingExceptionBase {
+ }
+
+ protected abstract U generateAlreadyExistsException(final M entity, final InternalCallContext context);
+
+ protected String getNaturalOrderingColumns() {
+ return "record_id";
+ }
+
+ @Override
+ public Long getRecordId(final UUID id, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Long>() {
+
+ @Override
+ public Long inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final EntitySqlDao<M, E> transactional = entitySqlDaoWrapperFactory.become(realSqlDao);
+ return transactional.getRecordId(id.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public M getByRecordId(final Long recordId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<M>() {
+
+ @Override
+ public M inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final EntitySqlDao<M, E> transactional = entitySqlDaoWrapperFactory.become(realSqlDao);
+ return transactional.getByRecordId(recordId, context);
+ }
+ });
+ }
+
+ @Override
+ public M getById(final UUID id, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<M>() {
+
+ @Override
+ public M inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final EntitySqlDao<M, E> transactional = entitySqlDaoWrapperFactory.become(realSqlDao);
+ return transactional.getById(id.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public Pagination<M> getAll(final InternalTenantContext context) {
+ // We usually always want to wrap our queries in an EntitySqlDaoTransactionWrapper... except here.
+ // Since we want to stream the results out, we don't want to auto-commit when this method returns.
+ final EntitySqlDao<M, E> sqlDao = transactionalSqlDao.onDemand(realSqlDao);
+
+ // Note: we need to perform the count before streaming the results, as the connection
+ // will be busy as we stream the results out. This is also why we cannot use
+ // SQL_CALC_FOUND_ROWS / FOUND_ROWS (which may not be faster anyways).
+ final Long count = sqlDao.getCount(context);
+
+ final Iterator<M> results = sqlDao.getAll(context);
+ return new DefaultPagination<M>(count, results);
+ }
+
+ @Override
+ public Pagination<M> get(final Long offset, final Long limit, final InternalTenantContext context) {
+ return paginationHelper.getPagination(realSqlDao,
+ new PaginationIteratorBuilder<M, E, EntitySqlDao<M, E>>() {
+ @Override
+ public Long getCount(final EntitySqlDao<M, E> sqlDao, final InternalTenantContext context) {
+ return sqlDao.getCount(context);
+ }
+
+ @Override
+ public Iterator<M> build(final EntitySqlDao<M, E> sqlDao, final Long limit, final InternalTenantContext context) {
+ return sqlDao.get(offset, limit, getNaturalOrderingColumns(), context);
+ }
+ },
+ offset,
+ limit,
+ context);
+ }
+
+ @Override
+ public Long getCount(final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Long>() {
+
+ @Override
+ public Long inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final EntitySqlDao<M, E> transactional = entitySqlDaoWrapperFactory.become(realSqlDao);
+ return transactional.getCount(context);
+ }
+ });
+ }
+
+ @Override
+ public void test(final InternalTenantContext context) {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final EntitySqlDao<M, E> transactional = entitySqlDaoWrapperFactory.become(realSqlDao);
+ transactional.test(context);
+ return null;
+ }
+ });
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntityModelDao.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntityModelDao.java
new file mode 100644
index 0000000..fff329f
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntityModelDao.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.util.entity.Entity;
+
+/**
+ * ModelDao classes represent the lowest level of Entity objects. There are used to generate
+ * SQL statements and retrieve objects from the database.
+ *
+ * @param <E> associated Entity object (used in EntitySqlDaoWrapperInvocationHandler)
+ */
+@SuppressWarnings("UnusedDeclaration")
+public interface EntityModelDao<E extends Entity> extends Entity {
+
+ /**
+ * Retrieve the TableName associated with this entity. This is used in
+ * EntitySqlDaoWrapperInvocationHandler for history and auditing purposes.
+ *
+ * @return the TableName object associated with this ModelDao entity
+ */
+ public TableName getTableName();
+
+
+ public TableName getHistoryTableName();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDao.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDao.java
new file mode 100644
index 0000000..65ed8b8
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDao.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.customizers.Define;
+import org.skife.jdbi.v2.sqlobject.mixins.CloseMe;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.commons.jdbi.statement.SmartFetchSize;
+import org.killbill.billing.entity.EntityPersistenceException;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.cache.Cachable;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CachableKey;
+import org.killbill.billing.util.dao.AuditSqlDao;
+import org.killbill.billing.util.dao.HistorySqlDao;
+import org.killbill.billing.util.entity.Entity;
+
+// TODO get rid of Transmogrifier, but code does not compile even if we create the
+// method public <T> T become(Class<T> typeToBecome); ?
+//
+@EntitySqlDaoStringTemplate
+public interface EntitySqlDao<M extends EntityModelDao<E>, E extends Entity> extends AuditSqlDao, HistorySqlDao<M, E>, Transmogrifier, Transactional<EntitySqlDao<M, E>>, CloseMe {
+
+ @SqlUpdate
+ @Audited(ChangeType.INSERT)
+ public void create(@BindBean final M entity,
+ @BindBean final InternalCallContext context) throws EntityPersistenceException;
+
+ @SqlQuery
+ public M getById(@Bind("id") final String id,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public M getByRecordId(@Bind("recordId") final Long recordId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public List<M> getByAccountRecordId(@BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public List<M> getByAccountRecordIdIncludedDeleted(@BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @Cachable(CacheType.RECORD_ID)
+ public Long getRecordId(@CachableKey(1) @Bind("id") final String id,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @SmartFetchSize(shouldStream = true)
+ public Iterator<M> search(@Bind("searchKey") final String searchKey,
+ @Bind("likeSearchKey") final String likeSearchKey,
+ @Bind("offset") final Long offset,
+ @Bind("rowCount") final Long rowCount,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public Long getSearchCount(@Bind("searchKey") final String searchKey,
+ @Bind("likeSearchKey") final String likeSearchKey,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @SmartFetchSize(shouldStream = true)
+ public Iterator<M> getAll(@BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ @SmartFetchSize(shouldStream = true)
+ public Iterator<M> get(@Bind("offset") final Long offset,
+ @Bind("rowCount") final Long rowCount,
+ @Define("orderBy") final String orderBy,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public Long getCount(@BindBean final InternalTenantContext context);
+
+ @SqlUpdate
+ public void test(@BindBean final InternalTenantContext context);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java
new file mode 100644
index 0000000..31bacaa
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.regex.Matcher;
+
+import org.skife.jdbi.v2.Query;
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.SqlStatementCustomizer;
+import org.skife.jdbi.v2.sqlobject.SqlStatementCustomizingAnnotation;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.StringTemplate3StatementLocator;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
+import org.skife.jdbi.v2.tweak.StatementLocator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.util.dao.LowerToCamelBeanMapperFactory;
+import org.killbill.billing.util.entity.Entity;
+
+@SqlStatementCustomizingAnnotation(EntitySqlDaoStringTemplate.EntitySqlDaoLocatorFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface EntitySqlDaoStringTemplate {
+
+ static final String DEFAULT_VALUE = " ~ ";
+
+ String value() default DEFAULT_VALUE;
+
+ public static class EntitySqlDaoLocatorFactory extends UseStringTemplate3StatementLocator.LocatorFactory {
+
+ final static boolean enableGroupTemplateCaching = Boolean.parseBoolean(System.getProperty("killbill.jdbi.allow.stringTemplateGroupCaching", "true"));
+
+ static ConcurrentMap<String, StatementLocator> locatorCache = new ConcurrentHashMap<String, StatementLocator>();
+
+ //
+ // This is only needed to compute the key for the cache -- whether we get a class or a pathname (string)
+ //
+ // (Similar to what jdbi is doing (StringTemplate3StatementLocator))
+ //
+ private final static String sep = "/"; // *Not* System.getProperty("file.separator"), which breaks in jars
+
+ public static String mungify(final Class claz) {
+ final String path = "/" + claz.getName();
+ return path.replaceAll("\\.", Matcher.quoteReplacement(sep)) + ".sql.stg";
+ }
+
+
+ private static StatementLocator getLocator(final String locatorPath) {
+
+ if (enableGroupTemplateCaching && locatorCache.containsKey(locatorPath)) {
+ return locatorCache.get(locatorPath);
+ }
+
+ final StringTemplate3StatementLocator.Builder builder = StringTemplate3StatementLocator.builder(locatorPath)
+ .shouldCache()
+ .withSuperGroup(EntitySqlDao.class)
+ .allowImplicitTemplateGroup()
+ .treatLiteralsAsTemplates();
+
+ final StatementLocator locator = builder.build();
+ if (enableGroupTemplateCaching) {
+ locatorCache.put(locatorPath, locator);
+ }
+ return locator;
+ }
+
+
+ public SqlStatementCustomizer createForType(final Annotation annotation, final Class sqlObjectType) {
+
+ final EntitySqlDaoStringTemplate a = (EntitySqlDaoStringTemplate) annotation;
+
+ final String locatorPath = DEFAULT_VALUE.equals(a.value()) ? mungify(sqlObjectType) : a.value();
+ final StatementLocator l = getLocator(locatorPath);
+ return new SqlStatementCustomizer() {
+ public void apply(final SQLStatement statement) {
+ statement.setStatementLocator(l);
+
+ if (statement instanceof Query) {
+ final Query query = (Query) statement;
+
+ // Find the model class associated with this sqlObjectType (which is a SqlDao class) to register its mapper
+ // If a custom mapper is defined via @RegisterMapper, don't register our generic one
+ if (sqlObjectType.getGenericInterfaces() != null &&
+ sqlObjectType.getAnnotation(RegisterMapper.class) == null) {
+ for (int i = 0; i < sqlObjectType.getGenericInterfaces().length; i++) {
+ if (sqlObjectType.getGenericInterfaces()[i] instanceof ParameterizedType) {
+ final ParameterizedType type = (ParameterizedType) sqlObjectType.getGenericInterfaces()[i];
+ for (int j = 0; j < type.getActualTypeArguments().length; j++) {
+ final Type modelType = type.getActualTypeArguments()[j];
+ if (modelType instanceof Class) {
+ final Class modelClazz = (Class) modelType;
+ if (Entity.class.isAssignableFrom(modelClazz)) {
+ query.registerMapper(new LowerToCamelBeanMapperFactory(modelClazz));
+ }
+ }
+ }
+ }
+
+ }
+ }
+ }
+ }
+ };
+ }
+
+ public SqlStatementCustomizer createForMethod(final Annotation annotation,
+ final Class sqlObjectType,
+ final Method method) {
+ throw new UnsupportedOperationException("Not Defined on Method");
+ }
+
+ public SqlStatementCustomizer createForParameter(final Annotation annotation,
+ final Class sqlObjectType,
+ final Method method,
+ final Object arg) {
+ throw new UnsupportedOperationException("Not defined on parameter");
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
new file mode 100644
index 0000000..594e6b9
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.Transaction;
+import org.skife.jdbi.v2.TransactionIsolationLevel;
+import org.skife.jdbi.v2.TransactionStatus;
+
+import org.killbill.clock.Clock;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.Entity;
+
+/**
+ * Transaction manager for EntitySqlDao queries
+ */
+public class EntitySqlDaoTransactionalJdbiWrapper {
+
+ private final IDBI dbi;
+ private final Clock clock;
+ private final CacheControllerDispatcher cacheControllerDispatcher;
+ private final NonEntityDao nonEntityDao;
+
+ public EntitySqlDaoTransactionalJdbiWrapper(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ this.dbi = dbi;
+ this.clock = clock;
+ this.cacheControllerDispatcher = cacheControllerDispatcher;
+ this.nonEntityDao = nonEntityDao;
+ }
+
+ class JdbiTransaction<ReturnType, M extends EntityModelDao<E>, E extends Entity> implements Transaction<ReturnType, EntitySqlDao<M, E>> {
+
+ private final EntitySqlDaoTransactionWrapper<ReturnType> entitySqlDaoTransactionWrapper;
+
+ JdbiTransaction(final EntitySqlDaoTransactionWrapper<ReturnType> entitySqlDaoTransactionWrapper) {
+ this.entitySqlDaoTransactionWrapper = entitySqlDaoTransactionWrapper;
+ }
+
+ @Override
+ public ReturnType inTransaction(final EntitySqlDao<M, E> transactionalSqlDao, final TransactionStatus status) throws Exception {
+ final EntitySqlDaoWrapperFactory<EntitySqlDao> factoryEntitySqlDao = new EntitySqlDaoWrapperFactory<EntitySqlDao>(transactionalSqlDao, clock, cacheControllerDispatcher, nonEntityDao);
+ return entitySqlDaoTransactionWrapper.inTransaction(factoryEntitySqlDao);
+ }
+ }
+
+ // To handle warnings only
+ interface InitialEntitySqlDao extends EntitySqlDao<EntityModelDao<Entity>, Entity> {}
+
+ /**
+ * @param entitySqlDaoTransactionWrapper transaction to execute
+ * @param <ReturnType> object type to return from the transaction
+ * @return result from the transaction fo type ReturnType
+ */
+ public <ReturnType> ReturnType execute(final EntitySqlDaoTransactionWrapper<ReturnType> entitySqlDaoTransactionWrapper) {
+ final EntitySqlDao<EntityModelDao<Entity>, Entity> entitySqlDao = dbi.onDemand(InitialEntitySqlDao.class);
+ return entitySqlDao.inTransaction(TransactionIsolationLevel.READ_COMMITTED, new JdbiTransaction<ReturnType, EntityModelDao<Entity>, Entity>(entitySqlDaoTransactionWrapper));
+ }
+
+ public <M extends EntityModelDao<E>, E extends Entity, T extends EntitySqlDao<M, E>> T onDemand(final Class<T> sqlObjectType) {
+ return dbi.onDemand(sqlObjectType);
+ }
+
+ /**
+ * @param entitySqlDaoTransactionWrapper transaction to execute
+ * @param <ReturnType> object type to return from the transaction
+ * @param <E> checked exception which can be thrown from the transaction
+ * @return result from the transaction fo type ReturnType
+ */
+ public <ReturnType, E extends Exception> ReturnType execute(final Class<E> exception, final EntitySqlDaoTransactionWrapper<ReturnType> entitySqlDaoTransactionWrapper) throws E {
+ try {
+ return execute(entitySqlDaoTransactionWrapper);
+ } catch (RuntimeException e) {
+ if (e.getCause() != null && e.getCause().getClass().isAssignableFrom(exception)) {
+ throw (E) e.getCause();
+ } else if (e.getCause() != null && e.getCause() instanceof RuntimeException) {
+ throw (RuntimeException) e.getCause();
+ } else {
+ throw e;
+ }
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionWrapper.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionWrapper.java
new file mode 100644
index 0000000..3452836
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionWrapper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+/**
+ * Transaction closure for EntitySqlDao queries
+ *
+ * @param <ReturnType> object type to return from the transaction
+ */
+public interface EntitySqlDaoTransactionWrapper<ReturnType> {
+
+ /**
+ * @param entitySqlDaoWrapperFactory factory to create EntitySqlDao instances
+ * @return result from the transaction of type ReturnType
+ */
+ ReturnType inTransaction(EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception;
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperFactory.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperFactory.java
new file mode 100644
index 0000000..1a56b04
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperFactory.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import java.lang.reflect.Proxy;
+
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.clock.Clock;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.Entity;
+
+/**
+ * Factory to create wrapped EntitySqlDao objects. During a transaction, make sure
+ * to create other EntitySqlDao objects via the #become call.
+ *
+ * @param <InitialSqlDao> EntitySqlDao type to create
+ * @see EntitySqlDaoWrapperInvocationHandler
+ */
+public class EntitySqlDaoWrapperFactory<InitialSqlDao extends EntitySqlDao> {
+
+ private final InitialSqlDao sqlDao;
+ private final Clock clock;
+ private final CacheControllerDispatcher cacheControllerDispatcher;
+
+ private final NonEntityDao nonEntityDao;
+
+ public EntitySqlDaoWrapperFactory(final InitialSqlDao sqlDao, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ this.sqlDao = sqlDao;
+ this.clock = clock;
+ this.cacheControllerDispatcher = cacheControllerDispatcher;
+ this.nonEntityDao = nonEntityDao;
+ }
+
+ /**
+ * Get an instance of a specified EntitySqlDao class, sharing the same database session as the
+ * initial sql dao class with which this wrapper factory was created.
+ *
+ * @param newSqlDaoClass the class to instantiate
+ * @param <NewSqlDao> EntitySqlDao type to create
+ * @return instance of NewSqlDao
+ */
+ public <NewSqlDao extends EntitySqlDao<NewEntityModelDao, NewEntity>,
+ NewEntityModelDao extends EntityModelDao<NewEntity>,
+ NewEntity extends Entity> NewSqlDao become(final Class<NewSqlDao> newSqlDaoClass) {
+ return create(newSqlDaoClass, sqlDao.become(newSqlDaoClass));
+ }
+
+ public <SelfType> SelfType transmogrify(final Class<SelfType> newTransactionalClass) {
+ return sqlDao.become(newTransactionalClass);
+ }
+
+ public InitialSqlDao getSqlDao() {
+ return sqlDao;
+ }
+
+ private <NewSqlDao extends EntitySqlDao<NewEntityModelDao, NewEntity>,
+ NewEntityModelDao extends EntityModelDao<NewEntity>,
+ NewEntity extends Entity> NewSqlDao create(final Class<NewSqlDao> newSqlDaoClass, final NewSqlDao newSqlDao) {
+ final ClassLoader classLoader = newSqlDao.getClass().getClassLoader();
+ final Class[] interfacesToImplement = {newSqlDaoClass};
+ final EntitySqlDaoWrapperInvocationHandler<NewSqlDao, NewEntityModelDao, NewEntity> wrapperInvocationHandler =
+ new EntitySqlDaoWrapperInvocationHandler<NewSqlDao, NewEntityModelDao, NewEntity>(newSqlDaoClass, newSqlDao, clock, cacheControllerDispatcher, nonEntityDao);
+
+ final Object newSqlDaoObject = Proxy.newProxyInstance(classLoader, interfacesToImplement, wrapperInvocationHandler);
+ return newSqlDaoClass.cast(newSqlDaoObject);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperInvocationHandler.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperInvocationHandler.java
new file mode 100644
index 0000000..f1a0153
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperInvocationHandler.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.skife.jdbi.v2.Binding;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.exceptions.DBIException;
+import org.skife.jdbi.v2.exceptions.StatementException;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.cache.Cachable;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CachableKey;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.cache.CacheLoaderArgument;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.util.dao.EntityAudit;
+import org.killbill.billing.util.dao.EntityHistoryModelDao;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.dao.NonEntitySqlDao;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.util.entity.Entity;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+
+/**
+ * Wraps an instance of EntitySqlDao, performing extra work around each method (Sql query)
+ *
+ * @param <S> EntitySqlDao type of the wrapped instance
+ * @param <M> EntityModel associated with S
+ * @param <E> Entity associated with M
+ */
+public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, M extends EntityModelDao<E>, E extends Entity> implements InvocationHandler {
+
+ public static final String CACHE_KEY_SEPARATOR = "::";
+
+ private final Logger logger = LoggerFactory.getLogger(EntitySqlDaoWrapperInvocationHandler.class);
+
+ private final Class<S> sqlDaoClass;
+ private final S sqlDao;
+
+ private final CacheControllerDispatcher cacheControllerDispatcher;
+ private final Clock clock;
+ private final NonEntityDao nonEntityDao;
+
+ public EntitySqlDaoWrapperInvocationHandler(final Class<S> sqlDaoClass, final S sqlDao, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ this.sqlDaoClass = sqlDaoClass;
+ this.sqlDao = sqlDao;
+ this.clock = clock;
+ this.cacheControllerDispatcher = cacheControllerDispatcher;
+ this.nonEntityDao = nonEntityDao;
+ }
+
+ @Override
+ public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
+ try {
+ return invokeSafely(proxy, method, args);
+ } catch (Throwable t) {
+ if (t.getCause() != null && t.getCause().getCause() != null && DBIException.class.isAssignableFrom(t.getCause().getClass())) {
+ // Likely a JDBC error, try to extract the SQL statement and JDBI bindings
+ if (t.getCause() instanceof StatementException) {
+ final StatementContext statementContext = ((StatementException) t.getCause()).getStatementContext();
+
+ if (statementContext != null) {
+ // Grumble, we need to rely on the suxxor toString() method as nothing is exposed
+ final Binding binding = statementContext.getBinding();
+
+ final PreparedStatement statement = statementContext.getStatement();
+ if (statement != null) {
+ // Note: we rely on the JDBC driver to have a sane toString() method...
+ errorDuringTransaction(t.getCause().getCause(), method, statement.toString() + "\n" + binding.toString());
+ } else {
+ errorDuringTransaction(t.getCause().getCause(), method, binding.toString());
+ }
+
+ // Never reached
+ return null;
+ }
+ }
+
+ errorDuringTransaction(t.getCause().getCause(), method);
+ } else if (t.getCause() != null) {
+ // t is likely not interesting (java.lang.reflect.InvocationTargetException)
+ errorDuringTransaction(t.getCause(), method);
+ } else {
+ errorDuringTransaction(t, method);
+ }
+ }
+
+ // Never reached
+ return null;
+ }
+
+ // Nice method name to ease debugging while looking at log files
+ private void errorDuringTransaction(final Throwable t, final Method method, final String extraErrorMessage) throws Throwable {
+ final StringBuilder errorMessageBuilder = new StringBuilder("Error during transaction for sql entity {} and method {}");
+ if (t instanceof SQLException) {
+ final SQLException sqlException = (SQLException) t;
+ errorMessageBuilder.append(" [SQL State: ")
+ .append(sqlException.getSQLState())
+ .append(", Vendor Error Code: ")
+ .append(sqlException.getErrorCode())
+ .append("]");
+ }
+ if (extraErrorMessage != null) {
+ // This is usually the SQL statement
+ errorMessageBuilder.append("\n").append(extraErrorMessage);
+ }
+ logger.warn(errorMessageBuilder.toString(), sqlDaoClass, method.getName());
+
+ // This is to avoid throwing an exception wrapped in an UndeclaredThrowableException
+ if (!(t instanceof RuntimeException)) {
+ throw new RuntimeException(t);
+ } else {
+ throw t;
+ }
+ }
+
+ private void errorDuringTransaction(final Throwable t, final Method method) throws Throwable {
+ errorDuringTransaction(t, method, null);
+ }
+
+ private Object invokeSafely(final Object proxy, final Method method, final Object[] args) throws Throwable {
+
+ final Audited auditedAnnotation = method.getAnnotation(Audited.class);
+ final Cachable cachableAnnotation = method.getAnnotation(Cachable.class);
+
+ // This can't be AUDIT'ed and CACHABLE'd at the same time as we only cache 'get'
+ if (auditedAnnotation != null) {
+ return invokeWithAuditAndHistory(auditedAnnotation, method, args);
+ } else if (cachableAnnotation != null) {
+ return invokeWithCaching(cachableAnnotation, method, args);
+ } else {
+ return method.invoke(sqlDao, args);
+ }
+ }
+
+ private Object invokeWithCaching(final Cachable cachableAnnotation, final Method method, final Object[] args)
+ throws IllegalAccessException, InvocationTargetException, ClassNotFoundException, InstantiationException {
+ final ObjectType objectType = getObjectType();
+ final CacheType cacheType = cachableAnnotation.value();
+ final CacheController<Object, Object> cache = cacheControllerDispatcher.getCacheController(cacheType);
+ Object result = null;
+ if (cache != null) {
+ // Find all arguments marked with @CachableKey
+ final Map<Integer, Object> keyPieces = new LinkedHashMap<Integer, Object>();
+ final Annotation[][] annotations = method.getParameterAnnotations();
+ for (int i = 0; i < annotations.length; i++) {
+ for (int j = 0; j < annotations[i].length; j++) {
+ final Annotation annotation = annotations[i][j];
+ if (CachableKey.class.equals(annotation.annotationType())) {
+ // CachableKey position starts at 1
+ keyPieces.put(((CachableKey) annotation).value() - 1, args[i]);
+ break;
+ }
+ }
+ }
+
+ // Build the Cache key
+ final String cacheKey = buildCacheKey(keyPieces);
+
+ final InternalTenantContext internalTenantContext = (InternalTenantContext) Iterables.find(ImmutableList.copyOf(args), new Predicate<Object>() {
+ @Override
+ public boolean apply(final Object input) {
+ return input instanceof InternalTenantContext;
+ }
+ }, null);
+ final CacheLoaderArgument cacheLoaderArgument = new CacheLoaderArgument(objectType, args, internalTenantContext);
+ result = cache.get(cacheKey, cacheLoaderArgument);
+ }
+ if (result == null) {
+ result = method.invoke(sqlDao, args);
+ }
+ return result;
+ }
+
+ /**
+ * Extract object from sqlDaoClass by looking at first parameter type (EntityModelDao) and
+ * constructing an empty object so we can call the getObjectType method on it.
+ *
+ * @return the objectType associated to that handler
+ * @throws InstantiationException
+ * @throws IllegalAccessException
+ * @throws ClassNotFoundException
+ */
+ private ObjectType getObjectType() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+
+ int foundIndexForEntitySqlDao = -1;
+ // If the sqlDaoClass implements multiple interfaces, first figure out which one is the EntitySqlDao
+ for (int i = 0; i < sqlDaoClass.getGenericInterfaces().length; i++) {
+ final Type type = sqlDaoClass.getGenericInterfaces()[0];
+ if (!(type instanceof java.lang.reflect.ParameterizedType)) {
+ // AuditSqlDao for example won't extend EntitySqlDao
+ return null;
+ }
+
+ if (EntitySqlDao.class.getName().equals(((Class) ((java.lang.reflect.ParameterizedType) type).getRawType()).getName())) {
+ foundIndexForEntitySqlDao = i;
+ break;
+ }
+ }
+ // Find out from the parameters of the EntitySqlDao which one is the EntityModelDao, and extract his (sub)type to finally return the ObjectType
+ if (foundIndexForEntitySqlDao >= 0) {
+ final Type[] types = ((java.lang.reflect.ParameterizedType) sqlDaoClass.getGenericInterfaces()[foundIndexForEntitySqlDao]).getActualTypeArguments();
+ int foundIndexForEntityModelDao = -1;
+ for (int i = 0; i < types.length; i++) {
+ final Class clz = ((Class) types[i]);
+ if (EntityModelDao.class.getName().equals(((Class) ((java.lang.reflect.ParameterizedType) clz.getGenericInterfaces()[0]).getRawType()).getName())) {
+ foundIndexForEntityModelDao = i;
+ break;
+ }
+ }
+
+ if (foundIndexForEntityModelDao >= 0) {
+ final String modelClassName = ((Class) types[foundIndexForEntityModelDao]).getName();
+
+ final Class<? extends EntityModelDao<?>> clz = (Class<? extends EntityModelDao<?>>) Class.forName(modelClassName);
+
+ final EntityModelDao<?> modelDao = (EntityModelDao<?>) clz.newInstance();
+ return modelDao.getTableName().getObjectType();
+ }
+ }
+ return null;
+ }
+
+
+ private Object invokeWithAuditAndHistory(final Audited auditedAnnotation, final Method method, final Object[] args) throws IllegalAccessException, InvocationTargetException {
+ InternalCallContext context = null;
+ List<String> entityIds = null;
+ final Map<String, M> entities = new HashMap<String, M>();
+ final Map<String, Long> entityRecordIds = new HashMap<String, Long>();
+ if (auditedAnnotation != null) {
+ // There will be some work required after the statement is executed,
+ // get the id before in case the change is a delete
+ context = retrieveContextFromArguments(args);
+ entityIds = retrieveEntityIdsFromArguments(method, args);
+ for (final String entityId : entityIds) {
+ entities.put(entityId, sqlDao.getById(entityId, context));
+ entityRecordIds.put(entityId, sqlDao.getRecordId(entityId, context));
+ }
+ }
+
+ // Real jdbc call
+ final Object obj = method.invoke(sqlDao, args);
+
+ final ChangeType changeType = auditedAnnotation.value();
+
+ for (final String entityId : entityIds) {
+ updateHistoryAndAudit(entityId, entities, entityRecordIds, changeType, context);
+ }
+ return obj;
+ }
+
+ private void updateHistoryAndAudit(final String entityId, final Map<String, M> entities, final Map<String, Long> entityRecordIds,
+ final ChangeType changeType, final InternalCallContext context) {
+ // Make sure to re-hydrate the object (especially needed for create calls)
+ final M reHydratedEntity = sqlDao.getById(entityId, context);
+ final Long reHydratedEntityRecordId = sqlDao.getRecordId(entityId, context);
+ final M entity = Objects.firstNonNull(reHydratedEntity, entities.get(entityId));
+ final Long entityRecordId = Objects.firstNonNull(reHydratedEntityRecordId, entityRecordIds.get(entityId));
+ final TableName tableName = entity.getTableName();
+
+ // Note: audit entries point to the history record id
+ final Long historyRecordId;
+ if (tableName.getHistoryTableName() != null) {
+ historyRecordId = insertHistory(entityRecordId, entity, changeType, context);
+ } else {
+ historyRecordId = entityRecordId;
+ }
+
+ insertAudits(tableName, entityRecordId, historyRecordId, changeType, context);
+ }
+
+ private List<String> retrieveEntityIdsFromArguments(final Method method, final Object[] args) {
+ final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+ int i = -1;
+ for (final Object arg : args) {
+ i++;
+
+ // Assume the first argument of type Entity is our type of Entity (type U here)
+ // This is true for e.g. create calls
+ if (arg instanceof Entity) {
+ return ImmutableList.<String>of(((Entity) arg).getId().toString());
+ }
+
+ // For Batch calls, the first argument will be of type List<Entity>
+ if (arg instanceof Iterable) {
+ final Builder<String> entityIds = extractEntityIdsFromBatchArgument((Iterable) arg);
+ if (entityIds != null) {
+ return entityIds.build();
+ }
+ }
+
+ // Otherwise, use the first String argument, annotated with @Bind("id")
+ // This is true for e.g. update calls
+ if (!(arg instanceof String)) {
+ continue;
+ }
+
+ for (final Annotation annotation : parameterAnnotations[i]) {
+ if (Bind.class.equals(annotation.annotationType()) && ("id").equals(((Bind) annotation).value())) {
+ return ImmutableList.<String>of((String) arg);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private Builder<String> extractEntityIdsFromBatchArgument(final Iterable arg) {
+ final Iterator iterator = arg.iterator();
+ final Builder<String> entityIds = new Builder<String>();
+ while (iterator.hasNext()) {
+ final Object object = iterator.next();
+ if (!(object instanceof Entity)) {
+ // No good - ignore
+ return null;
+ } else {
+ entityIds.add(((Entity) object).getId().toString());
+ }
+ }
+
+ return entityIds;
+ }
+
+
+ private InternalCallContext retrieveContextFromArguments(final Object[] args) {
+ for (final Object arg : args) {
+ if (!(arg instanceof InternalCallContext)) {
+ continue;
+ }
+ return (InternalCallContext) arg;
+ }
+ return null;
+ }
+
+ private Long insertHistory(final Long entityRecordId, final M entityModelDao, final ChangeType changeType, final InternalCallContext context) {
+ final EntityHistoryModelDao<M, E> history = new EntityHistoryModelDao<M, E>(entityModelDao, entityRecordId, changeType, clock.getUTCNow());
+
+ sqlDao.addHistoryFromTransaction(history, context);
+
+ final NonEntitySqlDao transactional = sqlDao.become(NonEntitySqlDao.class);
+
+ /* return transactional.getLastHistoryRecordId(entityRecordId, entityModelDao.getHistoryTableName().getTableName()); */
+ return nonEntityDao.retrieveLastHistoryRecordIdFromTransaction(entityRecordId, entityModelDao.getHistoryTableName(), transactional);
+ }
+
+ private void insertAudits(final TableName tableName, final Long entityRecordId, final Long historyRecordId, final ChangeType changeType, final InternalCallContext contextMaybeWithoutAccountRecordId) {
+ final TableName destinationTableName = Objects.firstNonNull(tableName.getHistoryTableName(), tableName);
+ final EntityAudit audit = new EntityAudit(destinationTableName, historyRecordId, changeType, clock.getUTCNow());
+
+ final InternalCallContext context;
+ // Populate the account record id when creating the account record
+ if (TableName.ACCOUNT.equals(tableName) && ChangeType.INSERT.equals(changeType)) {
+ context = new InternalCallContext(contextMaybeWithoutAccountRecordId, entityRecordId);
+ } else {
+ context = contextMaybeWithoutAccountRecordId;
+ }
+ sqlDao.insertAuditFromTransaction(audit, context);
+
+ // We need to invalidate the caches. There is a small window of doom here where caches will be stale.
+ // TODO Knowledge on how the key is constructed is also in AuditSqlDao
+ if (tableName.getHistoryTableName() != null) {
+ final CacheController<Object, Object> cacheController = cacheControllerDispatcher.getCacheController(CacheType.AUDIT_LOG_VIA_HISTORY);
+ if (cacheController != null) {
+ final String key = buildCacheKey(ImmutableMap.<Integer, Object>of(0, tableName.getHistoryTableName(), 1, tableName.getHistoryTableName(), 2, entityRecordId));
+ cacheController.remove(key);
+ }
+ } else {
+ final CacheController<Object, Object> cacheController = cacheControllerDispatcher.getCacheController(CacheType.AUDIT_LOG);
+ if (cacheController != null) {
+ final String key = buildCacheKey(ImmutableMap.<Integer, Object>of(0, tableName, 1, entityRecordId));
+ cacheController.remove(key);
+ }
+ }
+ }
+
+ private String buildCacheKey(final Map<Integer, Object> keyPieces) {
+ final StringBuilder cacheKey = new StringBuilder();
+ for (int i = 0; i < keyPieces.size(); i++) {
+ // To normalize the arguments and avoid casing issues, we make all pieces of the key uppercase.
+ // Since the database engine may be case insensitive and we use arguments of the SQL method call
+ // to build the key, the key has to be case insensitive as well.
+ final String str = String.valueOf(keyPieces.get(i)).toUpperCase();
+ cacheKey.append(str);
+ if (i < keyPieces.size() - 1) {
+ cacheKey.append(CACHE_KEY_SEPARATOR);
+ }
+ }
+ return cacheKey.toString();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java b/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java
new file mode 100644
index 0000000..3abdf37
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import com.google.common.collect.ImmutableList;
+
+// Assumes the original offset starts at zero.
+public class DefaultPagination<T> implements Pagination<T> {
+
+ private final Long currentOffset;
+ private final Long limit;
+ private final Long totalNbRecords;
+ private final Long maxNbRecords;
+ private final Iterator<T> delegateIterator;
+
+ // Builder when the streaming API can't be used (should only be used for tests)
+ // Notes: elements should be the entire records set (regardless of filtering) otherwise maxNbRecords won't be accurate
+ public static <T> Pagination<T> build(final Long offset, final Long limit, final Collection<T> elements) {
+ final List<T> allResults = ImmutableList.<T>copyOf(elements);
+
+ final List<T> results;
+ if (offset >= allResults.size()) {
+ results = ImmutableList.<T>of();
+ } else if (offset + limit > allResults.size()) {
+ results = allResults.subList(offset.intValue(), allResults.size());
+ } else {
+ results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue());
+ }
+ return new DefaultPagination<T>(offset, limit, (long) results.size(), (long) allResults.size(), results.iterator());
+ }
+
+ // Constructor for DAO -> API bridge
+ public DefaultPagination(final Pagination original, final Long limit, final Iterator<T> delegate) {
+ this(original.getCurrentOffset(), limit, original.getTotalNbRecords(), original.getMaxNbRecords(), delegate);
+ }
+
+ // Constructor for DAO getAll calls
+ public DefaultPagination(final Long maxNbRecords, final Iterator<T> results) {
+ this(0L, Long.MAX_VALUE, maxNbRecords, maxNbRecords, results);
+ }
+
+ public DefaultPagination(final Long currentOffset, final Long limit,
+ @Nullable final Long totalNbRecords, @Nullable final Long maxNbRecords,
+ final Iterator<T> delegateIterator) {
+ this.currentOffset = currentOffset;
+ this.limit = limit;
+ this.totalNbRecords = totalNbRecords;
+ this.maxNbRecords = maxNbRecords;
+ this.delegateIterator = delegateIterator;
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return delegateIterator;
+ }
+
+ @Override
+ public Long getCurrentOffset() {
+ return currentOffset;
+ }
+
+ @Override
+ public Long getNextOffset() {
+ final long candidate = currentOffset + limit;
+ if (totalNbRecords != null && candidate >= totalNbRecords) {
+ // No more results
+ return null;
+ } else {
+ // When we don't know the total number of records, the next offset
+ // returned here won't make sense once the last result is returned.
+ // It is the responsibility of the client to handle the pagination stop condition
+ // in that case (i.e. check if there is no more results).
+ return candidate;
+ }
+ }
+
+ @Override
+ public Long getMaxNbRecords() {
+ return maxNbRecords;
+ }
+
+ @Override
+ public Long getTotalNbRecords() {
+ return totalNbRecords;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultPagination{");
+ sb.append("currentOffset=").append(currentOffset);
+ sb.append(", nextOffset=").append(getNextOffset());
+ sb.append(", totalNbRecords=").append(totalNbRecords);
+ sb.append(", maxNbRecords=").append(maxNbRecords);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ // Expensive! Will compare the content of the iterator
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultPagination that = (DefaultPagination) o;
+
+ if (totalNbRecords != null ? !totalNbRecords.equals(that.totalNbRecords) : that.totalNbRecords != null) {
+ return false;
+ }
+ if (maxNbRecords != null ? !maxNbRecords.equals(that.maxNbRecords) : that.maxNbRecords != null) {
+ return false;
+ }
+ if (currentOffset != null ? !currentOffset.equals(that.currentOffset) : that.currentOffset != null) {
+ return false;
+ }
+ if (delegateIterator != null ? !ImmutableList.<T>copyOf(delegateIterator).equals(ImmutableList.<T>copyOf(that.delegateIterator)) : that.delegateIterator != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = currentOffset != null ? currentOffset.hashCode() : 0;
+ result = 31 * result + (totalNbRecords != null ? totalNbRecords.hashCode() : 0);
+ result = 31 * result + (maxNbRecords != null ? maxNbRecords.hashCode() : 0);
+ result = 31 * result + (delegateIterator != null ? delegateIterator.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/export/api/DefaultExportUserApi.java b/util/src/main/java/org/killbill/billing/util/export/api/DefaultExportUserApi.java
new file mode 100644
index 0000000..f86d123
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/export/api/DefaultExportUserApi.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.export.api;
+
+import java.io.OutputStream;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.util.api.DatabaseExportOutputStream;
+import org.killbill.billing.util.api.ExportUserApi;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.export.dao.CSVExportOutputStream;
+import org.killbill.billing.util.export.dao.DatabaseExportDao;
+
+public class DefaultExportUserApi implements ExportUserApi {
+
+ private final DatabaseExportDao exportDao;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ @Inject
+ public DefaultExportUserApi(final DatabaseExportDao exportDao,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.exportDao = exportDao;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
+ @Override
+ public void exportDataForAccount(final UUID accountId, final DatabaseExportOutputStream out, final CallContext context) {
+ final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(accountId, context);
+ exportDao.exportDataForAccount(out, internalContext);
+ }
+
+ @Override
+ public void exportDataAsCSVForAccount(final UUID accountId, final OutputStream out, final CallContext context) {
+ exportDataForAccount(accountId, new CSVExportOutputStream(out), context);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/export/dao/CSVExportOutputStream.java b/util/src/main/java/org/killbill/billing/util/export/dao/CSVExportOutputStream.java
new file mode 100644
index 0000000..2b0d14c
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/export/dao/CSVExportOutputStream.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.export.dao;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Map;
+
+import org.killbill.billing.util.api.ColumnInfo;
+import org.killbill.billing.util.api.DatabaseExportOutputStream;
+
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema.ColumnType;
+
+public class CSVExportOutputStream extends OutputStream implements DatabaseExportOutputStream {
+
+ private static final CsvMapper mapper = new CsvMapper();
+
+ private final OutputStream delegate;
+
+ private String currentTableName;
+ private CsvSchema currentCSVSchema;
+ private ObjectWriter writer;
+ private boolean shouldWriteHeader = false;
+
+ public CSVExportOutputStream(final OutputStream delegate) {
+ this.delegate = delegate;
+
+ // To be mysqlimport friendly with datetime type
+ mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ delegate.write(b);
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+
+ @Override
+ public void newTable(final String tableName, final List<ColumnInfo> columnsForTable) {
+ currentTableName = tableName;
+
+ final CsvSchema.Builder builder = CsvSchema.builder();
+ for (final ColumnInfo columnInfo : columnsForTable) {
+ builder.addColumn(columnInfo.getColumnName(), getColumnTypeFromSqlType(columnInfo.getDataType()));
+ }
+ currentCSVSchema = builder.build();
+
+ writer = mapper.writer(currentCSVSchema);
+ shouldWriteHeader = true;
+ }
+
+ @Override
+ public void write(final Map<String, Object> row) throws IOException {
+ final byte[] bytes;
+ if (shouldWriteHeader) {
+ // Write the header once (mapper.writer will clone the writer). Add a small marker in front of the header
+ // to easily split it
+ write(String.format("-- %s ", currentTableName).getBytes());
+ bytes = mapper.writer(currentCSVSchema.withHeader()).writeValueAsBytes(row);
+ shouldWriteHeader = false;
+ } else {
+ bytes = writer.writeValueAsBytes(row);
+ }
+
+ write(bytes);
+ }
+
+ private ColumnType getColumnTypeFromSqlType(final String dataType) {
+ if (dataType == null) {
+ return ColumnType.STRING;
+ } else if ("bigint".equals(dataType)) {
+ return ColumnType.NUMBER_OR_STRING;
+ } else if ("blob".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("char".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("date".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("datetime".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("decimal".equals(dataType)) {
+ return ColumnType.NUMBER_OR_STRING;
+ } else if ("enum".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("int".equals(dataType)) {
+ return ColumnType.NUMBER_OR_STRING;
+ } else if ("longblob".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("longtext".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("mediumblob".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("mediumtext".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("set".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("smallint".equals(dataType)) {
+ return ColumnType.NUMBER_OR_STRING;
+ } else if ("text".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("time".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("timestamp".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("tinyint".equals(dataType)) {
+ return ColumnType.NUMBER_OR_STRING;
+ } else if ("varbinary".equals(dataType)) {
+ return ColumnType.STRING;
+ } else if ("varchar".equals(dataType)) {
+ return ColumnType.STRING;
+ } else {
+ return ColumnType.STRING;
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/export/dao/DatabaseExportDao.java b/util/src/main/java/org/killbill/billing/util/export/dao/DatabaseExportDao.java
new file mode 100644
index 0000000..7343dbf
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/export/dao/DatabaseExportDao.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.export.dao;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.ResultIterator;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+
+import org.killbill.billing.util.api.ColumnInfo;
+import org.killbill.billing.util.api.DatabaseExportOutputStream;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.util.validation.DefaultColumnInfo;
+import org.killbill.billing.util.validation.dao.DatabaseSchemaDao;
+
+@Singleton
+public class DatabaseExportDao {
+
+ private final DatabaseSchemaDao databaseSchemaDao;
+ private final IDBI dbi;
+
+ @Inject
+ public DatabaseExportDao(final DatabaseSchemaDao databaseSchemaDao,
+ final IDBI dbi) {
+ this.databaseSchemaDao = databaseSchemaDao;
+ this.dbi = dbi;
+ }
+
+ public void exportDataForAccount(final DatabaseExportOutputStream out, final InternalTenantContext context) {
+ if (context.getAccountRecordId() == null || context.getTenantRecordId() == null) {
+ return;
+ }
+
+ final List<DefaultColumnInfo> columns = databaseSchemaDao.getColumnInfoList();
+ if (columns.size() == 0) {
+ return;
+ }
+
+ final List<ColumnInfo> columnsForTable = new ArrayList<ColumnInfo>();
+ // The list of columns is ordered by table name first
+ String lastSeenTableName = columns.get(0).getTableName();
+ for (final ColumnInfo column : columns) {
+ if (!column.getTableName().equals(lastSeenTableName)) {
+ exportDataForAccountAndTable(out, columnsForTable, context);
+ lastSeenTableName = column.getTableName();
+ columnsForTable.clear();
+ }
+ columnsForTable.add(column);
+ }
+ exportDataForAccountAndTable(out, columnsForTable, context);
+ }
+
+ private void exportDataForAccountAndTable(final DatabaseExportOutputStream out, final List<ColumnInfo> columnsForTable, final InternalTenantContext context) {
+ boolean hasAccountRecordIdColumn = false;
+ boolean firstColumn = true;
+ final StringBuilder queryBuilder = new StringBuilder("select ");
+ for (final ColumnInfo column : columnsForTable) {
+ if (!firstColumn) {
+ queryBuilder.append(", ");
+ } else {
+ firstColumn = false;
+ }
+
+ queryBuilder.append(column.getColumnName());
+ if (column.getColumnName().equals("account_record_id")) {
+ hasAccountRecordIdColumn = true;
+ }
+ }
+
+ final String tableName = columnsForTable.get(0).getTableName();
+ final boolean isAccountTable = TableName.ACCOUNT.getTableName().equals(tableName);
+
+ // Don't export non-account specific tables
+ if (!isAccountTable && !hasAccountRecordIdColumn) {
+ return;
+ }
+
+ // Build the query - make sure to filter by account and tenant!
+ queryBuilder.append(" from ")
+ .append(tableName);
+ if (isAccountTable) {
+ queryBuilder.append(" where record_id = :accountRecordId and tenant_record_id = :tenantRecordId");
+ } else {
+ queryBuilder.append(" where account_record_id = :accountRecordId and tenant_record_id = :tenantRecordId");
+ }
+
+ // Notify the stream that we're about to write data for a different table
+ out.newTable(tableName, columnsForTable);
+
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ final ResultIterator<Map<String, Object>> iterator = handle.createQuery(queryBuilder.toString())
+ .bind("accountRecordId", context.getAccountRecordId())
+ .bind("tenantRecordId", context.getTenantRecordId())
+ .iterator();
+ try {
+ while (iterator.hasNext()) {
+ final Map<String, Object> row = iterator.next();
+ out.write(row);
+ }
+ } finally {
+ iterator.close();
+ }
+
+ return null;
+ }
+ });
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/globallocker/LockerType.java b/util/src/main/java/org/killbill/billing/util/globallocker/LockerType.java
new file mode 100644
index 0000000..de8a0fe
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/globallocker/LockerType.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.globallocker;
+
+public enum LockerType {
+ ACCOUNT_FOR_INVOICE_PAYMENTS
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/AuditModule.java b/util/src/main/java/org/killbill/billing/util/glue/AuditModule.java
new file mode 100644
index 0000000..27fcc81
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/AuditModule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.audit.api.DefaultAuditUserApi;
+import org.killbill.billing.util.audit.dao.AuditDao;
+import org.killbill.billing.util.audit.dao.DefaultAuditDao;
+
+import com.google.inject.AbstractModule;
+
+public class AuditModule extends AbstractModule {
+
+ protected void installDaos() {
+ bind(AuditDao.class).to(DefaultAuditDao.class).asEagerSingleton();
+ }
+
+ protected void installUserApi() {
+ bind(AuditUserApi.class).to(DefaultAuditUserApi.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ installDaos();
+ installUserApi();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/BusModule.java b/util/src/main/java/org/killbill/billing/util/glue/BusModule.java
new file mode 100644
index 0000000..0e81214
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/BusModule.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.bus.InMemoryPersistentBus;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.bus.api.PersistentBusConfig;
+import org.killbill.billing.util.bus.DefaultBusService;
+import org.killbill.billing.util.svcsapi.bus.BusService;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.AbstractModule;
+
+public class BusModule extends AbstractModule {
+
+ private final BusType type;
+ private final ConfigSource configSource;
+
+ public BusModule(final ConfigSource configSource) {
+ this(BusType.PERSISTENT, configSource);
+ }
+
+ protected BusModule(final BusType type, final ConfigSource configSource) {
+ this.type = type;
+ this.configSource = configSource;
+ }
+
+ public enum BusType {
+ MEMORY,
+ PERSISTENT
+ }
+
+ @Override
+ protected void configure() {
+ bind(BusService.class).to(DefaultBusService.class);
+ switch (type) {
+ case MEMORY:
+ configureInMemoryEventBus();
+ break;
+ case PERSISTENT:
+ configurePersistentEventBus();
+ break;
+ default:
+ throw new RuntimeException("Unrecognized EventBus type " + type);
+ }
+ }
+
+ protected void configurePersistentEventBus() {
+ final PersistentBusConfig busConfig = new ConfigurationObjectFactory(configSource).buildWithReplacements(PersistentBusConfig.class,
+ ImmutableMap.<String, String>of("instanceName", "main"));
+ bind(BusProvider.class).toInstance(new BusProvider(busConfig));
+ bind(PersistentBus.class).toProvider(BusProvider.class).asEagerSingleton();
+ }
+
+ private void configureInMemoryEventBus() {
+ bind(PersistentBus.class).to(InMemoryPersistentBus.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/BusProvider.java b/util/src/main/java/org/killbill/billing/util/glue/BusProvider.java
new file mode 100644
index 0000000..bc392e6
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/BusProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.bus.DefaultPersistentBus;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.bus.api.PersistentBusConfig;
+import org.killbill.clock.Clock;
+
+import com.codahale.metrics.MetricRegistry;
+
+
+public class BusProvider implements Provider<PersistentBus> {
+
+ private final PersistentBusConfig busConfig;
+
+ private IDBI dbi;
+ private Clock clock;
+ private MetricRegistry metricRegistry;
+
+ public BusProvider(final PersistentBusConfig busConfig) {
+ this.busConfig = busConfig;
+ }
+
+ @Inject
+ public void initialize(final IDBI dbi, final Clock clock, final MetricRegistry metricRegistry) {
+ this.dbi = dbi;
+ this.clock = clock;
+ this.metricRegistry = metricRegistry;
+ }
+
+ @Override
+ public PersistentBus get() {
+ return new DefaultPersistentBus(dbi, clock, busConfig, metricRegistry);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java b/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
new file mode 100644
index 0000000..fbcff27
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.cache.CacheControllerDispatcherProvider;
+import org.killbill.billing.util.cache.EhCacheCacheManagerProvider;
+import org.killbill.billing.util.config.CacheConfig;
+
+import com.google.inject.AbstractModule;
+import net.sf.ehcache.CacheManager;
+
+public class CacheModule extends AbstractModule {
+
+ private final ConfigSource configSource;
+
+ public CacheModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ @Override
+ protected void configure() {
+ final CacheConfig config = new ConfigurationObjectFactory(configSource).build(CacheConfig.class);
+ bind(CacheConfig.class).toInstance(config);
+
+ // EhCache specifics
+ bind(CacheManager.class).toProvider(EhCacheCacheManagerProvider.class).asEagerSingleton();
+
+ // Kill Bill generic cache dispatcher
+ bind(CacheControllerDispatcher.class).toProvider(CacheControllerDispatcherProvider.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/CallContextModule.java b/util/src/main/java/org/killbill/billing/util/glue/CallContextModule.java
new file mode 100644
index 0000000..7267e0b
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/CallContextModule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.billing.util.callcontext.CallContextFactory;
+import org.killbill.billing.util.callcontext.DefaultCallContextFactory;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+
+import com.google.inject.AbstractModule;
+
+public class CallContextModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(CallContextFactory.class).to(DefaultCallContextFactory.class).asEagerSingleton();
+ bind(InternalCallContextFactory.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/ClockModule.java b/util/src/main/java/org/killbill/billing/util/glue/ClockModule.java
new file mode 100644
index 0000000..bf90f7f
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/ClockModule.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.clock.Clock;
+import org.killbill.clock.DefaultClock;
+
+import com.google.inject.AbstractModule;
+
+public class ClockModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(Clock.class).to(DefaultClock.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/CustomFieldModule.java b/util/src/main/java/org/killbill/billing/util/glue/CustomFieldModule.java
new file mode 100644
index 0000000..f689e9a
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/CustomFieldModule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.customfield.api.DefaultCustomFieldUserApi;
+import org.killbill.billing.util.customfield.dao.DefaultCustomFieldDao;
+import org.killbill.billing.util.customfield.dao.CustomFieldDao;
+
+import com.google.inject.AbstractModule;
+
+public class CustomFieldModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ installCustomFieldDao();
+ installCustomFieldUserApi();
+ }
+
+ protected void installCustomFieldUserApi() {
+ bind(CustomFieldUserApi.class).to(DefaultCustomFieldUserApi.class).asEagerSingleton();
+ }
+
+ protected void installCustomFieldDao() {
+ bind(CustomFieldDao.class).to(DefaultCustomFieldDao.class).asEagerSingleton();
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java b/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java
new file mode 100644
index 0000000..bd742f8
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.apache.shiro.cache.ehcache.EhCacheManager;
+import org.apache.shiro.mgt.DefaultSecurityManager;
+import org.apache.shiro.mgt.SecurityManager;
+
+import net.sf.ehcache.CacheManager;
+
+public class EhCacheManagerProvider implements Provider<EhCacheManager> {
+
+ private final SecurityManager securityManager;
+ private final CacheManager ehCacheCacheManager;
+
+ @Inject
+ public EhCacheManagerProvider(final SecurityManager securityManager, final CacheManager ehCacheCacheManager) {
+ this.securityManager = securityManager;
+ this.ehCacheCacheManager = ehCacheCacheManager;
+ }
+
+ @Override
+ public EhCacheManager get() {
+ final EhCacheManager shiroEhCacheManager = new EhCacheManager();
+ // Same EhCache manager instance as the rest of the system
+ shiroEhCacheManager.setCacheManager(ehCacheCacheManager);
+
+ if (securityManager instanceof DefaultSecurityManager) {
+ ((DefaultSecurityManager) securityManager).setCacheManager(shiroEhCacheManager);
+ }
+
+ return shiroEhCacheManager;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/ExportModule.java b/util/src/main/java/org/killbill/billing/util/glue/ExportModule.java
new file mode 100644
index 0000000..3280dcd
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/ExportModule.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.billing.util.api.ExportUserApi;
+import org.killbill.billing.util.export.api.DefaultExportUserApi;
+
+import com.google.inject.AbstractModule;
+
+public class ExportModule extends AbstractModule {
+
+ protected void installUserApi() {
+ bind(ExportUserApi.class).to(DefaultExportUserApi.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ installUserApi();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/GlobalLockerModule.java b/util/src/main/java/org/killbill/billing/util/glue/GlobalLockerModule.java
new file mode 100644
index 0000000..3bcd3e8
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/GlobalLockerModule.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.commons.embeddeddb.EmbeddedDB;
+import org.killbill.commons.embeddeddb.EmbeddedDB.DBEngine;
+
+import com.google.inject.AbstractModule;
+
+public class GlobalLockerModule extends AbstractModule {
+
+ private final DBEngine engine;
+
+ public GlobalLockerModule(final DBEngine engine) {
+ this.engine = engine;
+ }
+
+ @Override
+ protected void configure() {
+ if (EmbeddedDB.DBEngine.MYSQL.equals(engine)) {
+ install(new MySqlGlobalLockerModule());
+ } else {
+ install(new MemoryGlobalLockerModule());
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java b/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java
new file mode 100644
index 0000000..3bbf599
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.apache.shiro.config.ConfigurationException;
+import org.apache.shiro.realm.text.IniRealm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.util.config.SecurityConfig;
+
+public class IniRealmProvider implements Provider<IniRealm> {
+
+ private static final Logger log = LoggerFactory.getLogger(IniRealmProvider.class);
+
+ private final SecurityConfig securityConfig;
+
+ @Inject
+ public IniRealmProvider(final SecurityConfig securityConfig) {
+ this.securityConfig = securityConfig;
+ }
+
+ @Override
+ public IniRealm get() {
+ try {
+ return new IniRealm(securityConfig.getShiroResourcePath());
+ } catch (ConfigurationException e) {
+ log.warn("Unable to configure RBAC", e);
+ return new IniRealm();
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/JDBCSessionDaoProvider.java b/util/src/main/java/org/killbill/billing/util/glue/JDBCSessionDaoProvider.java
new file mode 100644
index 0000000..f9c6290
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/JDBCSessionDaoProvider.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.apache.shiro.session.mgt.DefaultSessionManager;
+import org.apache.shiro.session.mgt.SessionManager;
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.util.config.RbacConfig;
+import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
+
+public class JDBCSessionDaoProvider implements Provider<JDBCSessionDao> {
+
+ private final SessionManager sessionManager;
+ private final IDBI dbi;
+ private final RbacConfig rbacConfig;
+
+ @Inject
+ public JDBCSessionDaoProvider(final IDBI dbi, final SessionManager sessionManager, final RbacConfig rbacConfig) {
+ this.sessionManager = sessionManager;
+ this.dbi = dbi;
+ this.rbacConfig = rbacConfig;
+ }
+
+ @Override
+ public JDBCSessionDao get() {
+ final JDBCSessionDao jdbcSessionDao = new JDBCSessionDao(dbi);
+
+ if (sessionManager instanceof DefaultSessionManager) {
+ final DefaultSessionManager defaultSessionManager = (DefaultSessionManager) sessionManager;
+ defaultSessionManager.setSessionDAO(jdbcSessionDao);
+ defaultSessionManager.setGlobalSessionTimeout(rbacConfig.getGlobalSessionTimeout().getMillis());
+ }
+
+ return jdbcSessionDao;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroAopModule.java b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroAopModule.java
new file mode 100644
index 0000000..25768c7
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroAopModule.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+
+import org.apache.shiro.aop.AnnotationMethodInterceptor;
+import org.apache.shiro.aop.AnnotationResolver;
+import org.apache.shiro.guice.aop.ShiroAopModule;
+
+import org.killbill.billing.util.security.AnnotationHierarchicalResolver;
+import org.killbill.billing.util.security.AopAllianceMethodInterceptorAdapter;
+import org.killbill.billing.util.security.PermissionAnnotationHandler;
+import org.killbill.billing.util.security.PermissionAnnotationMethodInterceptor;
+
+import com.google.inject.matcher.AbstractMatcher;
+import com.google.inject.matcher.Matchers;
+
+// Provides authentication via Shiro
+public class KillBillShiroAopModule extends ShiroAopModule {
+
+ private final AnnotationHierarchicalResolver resolver = new AnnotationHierarchicalResolver();
+
+ @Override
+ protected AnnotationResolver createAnnotationResolver() {
+ return resolver;
+ }
+
+ @Override
+ protected void configureInterceptors(final AnnotationResolver resolver) {
+ super.configureInterceptors(resolver);
+
+ if (!KillBillShiroModule.isRBACEnabled()) {
+ return;
+ }
+
+ final PermissionAnnotationHandler permissionAnnotationHandler = new PermissionAnnotationHandler();
+ // Inject the Security API
+ requestInjection(permissionAnnotationHandler);
+
+ final PermissionAnnotationMethodInterceptor methodInterceptor = new PermissionAnnotationMethodInterceptor(permissionAnnotationHandler, resolver);
+ bindShiroInterceptorWithHierarchy(methodInterceptor);
+ }
+
+ // Similar to bindShiroInterceptor but will look for annotations in the class hierarchy
+ protected final void bindShiroInterceptorWithHierarchy(final AnnotationMethodInterceptor methodInterceptor) {
+ bindInterceptor(Matchers.any(),
+ new AbstractMatcher<Method>() {
+ public boolean matches(final Method method) {
+ final Class<? extends Annotation> annotation = methodInterceptor.getHandler().getAnnotationClass();
+ return resolver.getAnnotationFromMethod(method, annotation) != null;
+ }
+ },
+ new AopAllianceMethodInterceptorAdapter(methodInterceptor));
+ }
+}
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
new file mode 100644
index 0000000..cdb5561
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.apache.shiro.cache.CacheManager;
+import org.apache.shiro.guice.ShiroModule;
+import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.session.mgt.DefaultSessionManager;
+import org.apache.shiro.session.mgt.SessionManager;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.billing.util.config.RbacConfig;
+import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
+import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
+
+import com.google.inject.binder.AnnotatedBindingBuilder;
+
+// For Kill Bill library only.
+// See org.killbill.billing.server.modules.KillBillShiroWebModule for Kill Bill server.
+public class KillBillShiroModule extends ShiroModule {
+
+ public static final String KILLBILL_LDAP_PROPERTY = "killbill.server.ldap";
+ public static final String KILLBILL_RBAC_PROPERTY = "killbill.server.rbac";
+
+ public static boolean isLDAPEnabled() {
+ return Boolean.parseBoolean(System.getProperty(KILLBILL_LDAP_PROPERTY, "false"));
+ }
+
+ public static boolean isRBACEnabled() {
+ return Boolean.parseBoolean(System.getProperty(KILLBILL_RBAC_PROPERTY, "true"));
+ }
+
+ private final ConfigSource configSource;
+
+ public KillBillShiroModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ protected void configureShiro() {
+ final RbacConfig config = new ConfigurationObjectFactory(configSource).build(RbacConfig.class);
+ bind(RbacConfig.class).toInstance(config);
+
+ bindRealm().toProvider(IniRealmProvider.class).asEagerSingleton();
+
+ if (isLDAPEnabled()) {
+ bindRealm().to(KillBillJndiLdapRealm.class).asEagerSingleton();
+ }
+ }
+
+ @Override
+ protected void bindSecurityManager(final AnnotatedBindingBuilder<? super SecurityManager> bind) {
+ super.bindSecurityManager(bind);
+
+ // Magic provider to configure the cache manager
+ bind(CacheManager.class).toProvider(EhCacheManagerProvider.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void bindSessionManager(final AnnotatedBindingBuilder<SessionManager> bind) {
+ bind.to(DefaultSessionManager.class).asEagerSingleton();
+
+ // Magic provider to configure the session DAO
+ bind(JDBCSessionDao.class).toProvider(JDBCSessionDaoProvider.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/MemoryGlobalLockerModule.java b/util/src/main/java/org/killbill/billing/util/glue/MemoryGlobalLockerModule.java
new file mode 100644
index 0000000..bdb84cf
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/MemoryGlobalLockerModule.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.commons.locker.memory.MemoryGlobalLocker;
+
+import com.google.inject.AbstractModule;
+
+public class MemoryGlobalLockerModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(GlobalLocker.class).to(MemoryGlobalLocker.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/MetricsModule.java b/util/src/main/java/org/killbill/billing/util/glue/MetricsModule.java
new file mode 100644
index 0000000..a05178a
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/MetricsModule.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.inject.AbstractModule;
+
+public class MetricsModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(MetricRegistry.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/MySqlGlobalLockerModule.java b/util/src/main/java/org/killbill/billing/util/glue/MySqlGlobalLockerModule.java
new file mode 100644
index 0000000..5c2d426
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/MySqlGlobalLockerModule.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.commons.locker.GlobalLocker;
+
+import com.google.inject.AbstractModule;
+
+public class MySqlGlobalLockerModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(GlobalLocker.class).toProvider(MySqlGlobalLockerProvider.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/MySqlGlobalLockerProvider.java b/util/src/main/java/org/killbill/billing/util/glue/MySqlGlobalLockerProvider.java
new file mode 100644
index 0000000..dd05579
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/MySqlGlobalLockerProvider.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.sql.DataSource;
+
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.commons.locker.mysql.MySqlGlobalLocker;
+
+public class MySqlGlobalLockerProvider implements Provider<GlobalLocker> {
+
+ private final DataSource dataSource;
+
+ @Inject
+ public MySqlGlobalLockerProvider(final DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ @Override
+ public GlobalLocker get() {
+ return new MySqlGlobalLocker(dataSource);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/NonEntityDaoModule.java b/util/src/main/java/org/killbill/billing/util/glue/NonEntityDaoModule.java
new file mode 100644
index 0000000..e815878
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/NonEntityDaoModule.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.dao.DefaultNonEntityDao;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+import com.google.inject.AbstractModule;
+
+public class NonEntityDaoModule extends AbstractModule {
+
+
+ @Override
+ protected void configure() {
+ bind(NonEntityDao.class).to(DefaultNonEntityDao.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/NotificationQueueModule.java b/util/src/main/java/org/killbill/billing/util/glue/NotificationQueueModule.java
new file mode 100644
index 0000000..fd17d61
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/NotificationQueueModule.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.notificationq.DefaultNotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueConfig;
+import org.killbill.notificationq.api.NotificationQueueService;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.AbstractModule;
+
+public class NotificationQueueModule extends AbstractModule {
+
+ protected final ConfigSource configSource;
+
+ public NotificationQueueModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ protected void configureNotificationQueueConfig() {
+ final NotificationQueueConfig config = new ConfigurationObjectFactory(configSource).buildWithReplacements(NotificationQueueConfig.class,
+ ImmutableMap.<String, String>of("instanceName", "main"));
+ bind(NotificationQueueConfig.class).toInstance(config);
+ }
+
+ @Override
+ protected void configure() {
+ bind(NotificationQueueService.class).to(DefaultNotificationQueueService.class).asEagerSingleton();
+ configureNotificationQueueConfig();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/RecordIdModule.java b/util/src/main/java/org/killbill/billing/util/glue/RecordIdModule.java
new file mode 100644
index 0000000..a1b32b9
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/RecordIdModule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.billing.util.api.RecordIdApi;
+import org.killbill.billing.util.recordid.DefaultRecordIdApi;
+
+import com.google.inject.AbstractModule;
+
+public class RecordIdModule extends AbstractModule {
+
+
+ @Override
+ protected void configure() {
+ bind(RecordIdApi.class).to(DefaultRecordIdApi.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/SecurityModule.java b/util/src/main/java/org/killbill/billing/util/glue/SecurityModule.java
new file mode 100644
index 0000000..46482ab
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/SecurityModule.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.skife.config.SimplePropertyConfigSource;
+
+import org.killbill.billing.security.api.SecurityApi;
+import org.killbill.billing.util.config.SecurityConfig;
+import org.killbill.billing.util.security.api.DefaultSecurityApi;
+import org.killbill.billing.util.security.api.DefaultSecurityService;
+import org.killbill.billing.util.security.api.SecurityService;
+
+import com.google.inject.AbstractModule;
+
+public class SecurityModule extends AbstractModule {
+
+ private final ConfigSource configSource;
+
+ public SecurityModule() {
+ this(new SimplePropertyConfigSource(System.getProperties()));
+ }
+
+ public SecurityModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ public void configure() {
+ installConfig();
+ installSecurityApi();
+ installSecurityService();
+ }
+
+ private void installConfig() {
+ final SecurityConfig securityConfig = new ConfigurationObjectFactory(configSource).build(SecurityConfig.class);
+ bind(SecurityConfig.class).toInstance(securityConfig);
+ }
+
+ private void installSecurityApi() {
+ bind(SecurityApi.class).to(DefaultSecurityApi.class).asEagerSingleton();
+ }
+
+ protected void installSecurityService() {
+ bind(SecurityService.class).to(DefaultSecurityService.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/TagStoreModule.java b/util/src/main/java/org/killbill/billing/util/glue/TagStoreModule.java
new file mode 100644
index 0000000..f5b97a1
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/TagStoreModule.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.tag.DefaultTagInternalApi;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.tag.api.DefaultTagUserApi;
+import org.killbill.billing.util.tag.dao.DefaultTagDao;
+import org.killbill.billing.util.tag.dao.DefaultTagDefinitionDao;
+import org.killbill.billing.util.tag.dao.TagDao;
+import org.killbill.billing.util.tag.dao.TagDefinitionDao;
+
+import com.google.inject.AbstractModule;
+
+public class TagStoreModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ installUserApi();
+ installInternalApi();
+ installDaos();
+ }
+
+ protected void installUserApi() {
+ bind(TagUserApi.class).to(DefaultTagUserApi.class).asEagerSingleton();
+ }
+
+ protected void installInternalApi() {
+ bind(TagInternalApi.class).to(DefaultTagInternalApi.class).asEagerSingleton();
+ }
+
+ protected void installDaos() {
+ bind(TagDefinitionDao.class).to(DefaultTagDefinitionDao.class).asEagerSingleton();
+ bind(TagDao.class).to(DefaultTagDao.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/Hostname.java b/util/src/main/java/org/killbill/billing/util/Hostname.java
new file mode 100644
index 0000000..1630be0
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/Hostname.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+public class Hostname {
+
+ public static String get() {
+ try {
+ final InetAddress addr = InetAddress.getLocalHost();
+ return addr.getHostName();
+ } catch (UnknownHostException e) {
+ e.printStackTrace();
+ return "hostname-unknown";
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/io/IOUtils.java b/util/src/main/java/org/killbill/billing/util/io/IOUtils.java
new file mode 100644
index 0000000..9a75fa2
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/io/IOUtils.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharStreams;
+import com.google.common.io.InputSupplier;
+
+public class IOUtils {
+ public static String toString(final InputStream stream) throws IOException {
+ final InputSupplier<InputStream> inputSupplier = new InputSupplier<InputStream>() {
+ @Override
+ public InputStream getInput() throws IOException {
+ return stream;
+ }
+ };
+
+ return CharStreams.toString(CharStreams.newReaderSupplier(inputSupplier, Charsets.UTF_8));
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java b/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java
new file mode 100644
index 0000000..2e69423
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.jackson;
+
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+
+public class ObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper {
+ public ObjectMapper() {
+ this.registerModule(new JodaModule());
+ this.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/LocaleUtils.java b/util/src/main/java/org/killbill/billing/util/LocaleUtils.java
new file mode 100644
index 0000000..5428003
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/LocaleUtils.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util;
+
+import java.util.Locale;
+
+public class LocaleUtils {
+
+ private LocaleUtils() {
+ }
+
+ // From commons-lang
+ public static Locale toLocale(final String str) {
+ if (str == null) {
+ return null;
+ }
+ final int len = str.length();
+ if (len != 2 && len != 5 && len < 7) {
+ throw new IllegalArgumentException("Invalid locale format: " + str);
+ }
+ final char ch0 = str.charAt(0);
+ final char ch1 = str.charAt(1);
+ if (ch0 < 'a' || ch0 > 'z' || ch1 < 'a' || ch1 > 'z') {
+ throw new IllegalArgumentException("Invalid locale format: " + str);
+ }
+ if (len == 2) {
+ return new Locale(str, "");
+ } else {
+ if (str.charAt(2) != '_') {
+ throw new IllegalArgumentException("Invalid locale format: " + str);
+ }
+ final char ch3 = str.charAt(3);
+ if (ch3 == '_') {
+ return new Locale(str.substring(0, 2), "", str.substring(4));
+ }
+ final char ch4 = str.charAt(4);
+ if (ch3 < 'A' || ch3 > 'Z' || ch4 < 'A' || ch4 > 'Z') {
+ throw new IllegalArgumentException("Invalid locale format: " + str);
+ }
+ if (len == 5) {
+ return new Locale(str.substring(0, 2), str.substring(3, 5));
+ } else {
+ if (str.charAt(5) != '_') {
+ throw new IllegalArgumentException("Invalid locale format: " + str);
+ }
+ return new Locale(str.substring(0, 2), str.substring(3, 5), str.substring(6));
+ }
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/recordid/DefaultRecordIdApi.java b/util/src/main/java/org/killbill/billing/util/recordid/DefaultRecordIdApi.java
new file mode 100644
index 0000000..cc8a7ab
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/recordid/DefaultRecordIdApi.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.recordid;
+
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.api.RecordIdApi;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.dao.NonEntityDao;
+
+public class DefaultRecordIdApi implements RecordIdApi {
+
+ private final NonEntityDao nonEntityDao;
+ private final CacheControllerDispatcher cacheControllerDispatcher;
+
+ @Inject
+ public DefaultRecordIdApi(final NonEntityDao nonEntityDao, final CacheControllerDispatcher cacheControllerDispatcher) {
+ this.nonEntityDao = nonEntityDao;
+ this.cacheControllerDispatcher = cacheControllerDispatcher;
+ }
+
+
+ @Override
+ public Long getRecordId(final UUID objectId, final ObjectType objectType, final TenantContext tenantContext) {
+ return nonEntityDao.retrieveRecordIdFromObject(objectId, objectType, cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID));
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java b/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java
new file mode 100644
index 0000000..3973932
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.apache.shiro.aop.AnnotationResolver;
+import org.apache.shiro.aop.MethodInvocation;
+
+public class AnnotationHierarchicalResolver implements AnnotationResolver {
+
+ @Override
+ public Annotation getAnnotation(final MethodInvocation mi, final Class<? extends Annotation> clazz) {
+ return getAnnotationFromMethod(mi.getMethod(), clazz);
+ }
+
+ public Annotation getAnnotationFromMethod(final Method method, final Class<? extends Annotation> clazz) {
+ return findAnnotation(method, clazz);
+ }
+
+ // The following comes from spring-core (AnnotationUtils) to handle annotations on interfaces
+
+ /**
+ * Get a single {@link Annotation} of <code>annotationType</code> from the supplied {@link java.lang.reflect.Method},
+ * traversing its super methods if no annotation can be found on the given method itself.
+ * <p>Annotations on methods are not inherited by default, so we need to handle this explicitly.
+ *
+ * @param method the method to look for annotations on
+ * @param annotationType the annotation class to look for
+ * @return the annotation found, or <code>null</code> if none found
+ */
+ public static <A extends Annotation> A findAnnotation(final Method method, final Class<A> annotationType) {
+ A annotation = getAnnotation(method, annotationType);
+ Class<?> cl = method.getDeclaringClass();
+ if (annotation == null) {
+ annotation = searchOnInterfaces(method, annotationType, cl.getInterfaces());
+ }
+ while (annotation == null) {
+ cl = cl.getSuperclass();
+ if (cl == null || cl == Object.class) {
+ break;
+ }
+ try {
+ final Method equivalentMethod = cl.getDeclaredMethod(method.getName(), method.getParameterTypes());
+ annotation = getAnnotation(equivalentMethod, annotationType);
+ if (annotation == null) {
+ annotation = searchOnInterfaces(method, annotationType, cl.getInterfaces());
+ }
+ } catch (NoSuchMethodException ex) {
+ // We're done...
+ }
+ }
+ return annotation;
+ }
+
+ /**
+ * Get a single {@link Annotation} of <code>annotationType</code> from the supplied {@link Method}.
+ *
+ * @param method the method to look for annotations on
+ * @param annotationType the annotation class to look for
+ * @return the annotations found
+ */
+ public static <A extends Annotation> A getAnnotation(final Method method, final Class<A> annotationType) {
+ A ann = method.getAnnotation(annotationType);
+ if (ann == null) {
+ for (final Annotation metaAnn : method.getAnnotations()) {
+ ann = metaAnn.annotationType().getAnnotation(annotationType);
+ if (ann != null) {
+ break;
+ }
+ }
+ }
+ return ann;
+ }
+
+ private static <A extends Annotation> A searchOnInterfaces(final Method method, final Class<A> annotationType, final Class<?>[] ifcs) {
+ A annotation = null;
+ for (final Class<?> iface : ifcs) {
+ if (isInterfaceWithAnnotatedMethods(iface)) {
+ try {
+ final Method equivalentMethod = iface.getMethod(method.getName(), method.getParameterTypes());
+ annotation = getAnnotation(equivalentMethod, annotationType);
+ } catch (NoSuchMethodException ex) {
+ // Skip this interface - it doesn't have the method...
+ }
+ if (annotation != null) {
+ break;
+ }
+ }
+ }
+ return annotation;
+ }
+
+ private static final Map<Class<?>, Boolean> annotatedInterfaceCache = new WeakHashMap<Class<?>, Boolean>();
+
+ private static boolean isInterfaceWithAnnotatedMethods(final Class<?> iface) {
+ synchronized (annotatedInterfaceCache) {
+ final Boolean flag = annotatedInterfaceCache.get(iface);
+ if (flag != null) {
+ return flag;
+ }
+ boolean found = false;
+ for (final Method ifcMethod : iface.getMethods()) {
+ if (ifcMethod.getAnnotations().length > 0) {
+ found = true;
+ break;
+ }
+ }
+ annotatedInterfaceCache.put(iface, found);
+ return found;
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/AopAllianceMethodInterceptorAdapter.java b/util/src/main/java/org/killbill/billing/util/security/AopAllianceMethodInterceptorAdapter.java
new file mode 100644
index 0000000..2ba5199
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/AopAllianceMethodInterceptorAdapter.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+// Taken from Shiro - the original class is private :(
+public class AopAllianceMethodInterceptorAdapter implements MethodInterceptor {
+
+ org.apache.shiro.aop.MethodInterceptor shiroInterceptor;
+
+ public AopAllianceMethodInterceptorAdapter(final org.apache.shiro.aop.MethodInterceptor shiroInterceptor) {
+ this.shiroInterceptor = shiroInterceptor;
+ }
+
+ public Object invoke(final MethodInvocation invocation) throws Throwable {
+ return shiroInterceptor.invoke(new AopAllianceMethodInvocationAdapter(invocation));
+ }
+
+ @Override
+ public String toString() {
+ return "AopAlliance Adapter for " + shiroInterceptor.toString();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/AopAllianceMethodInvocationAdapter.java b/util/src/main/java/org/killbill/billing/util/security/AopAllianceMethodInvocationAdapter.java
new file mode 100644
index 0000000..097bee4
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/AopAllianceMethodInvocationAdapter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security;
+
+import java.lang.reflect.Method;
+
+import org.aopalliance.intercept.MethodInvocation;
+
+// Taken from Shiro - the original class is private :(
+public class AopAllianceMethodInvocationAdapter implements org.apache.shiro.aop.MethodInvocation {
+
+ private final MethodInvocation mi;
+
+ public AopAllianceMethodInvocationAdapter(final MethodInvocation mi) {
+ this.mi = mi;
+ }
+
+ public Method getMethod() {
+ return mi.getMethod();
+ }
+
+ public Object[] getArguments() {
+ return mi.getArguments();
+ }
+
+ public String toString() {
+ return "Method invocation [" + mi.getMethod() + "]";
+ }
+
+ public Object proceed() throws Throwable {
+ return mi.proceed();
+ }
+
+ public Object getThis() {
+ return mi.getThis();
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/api/DefaultSecurityApi.java b/util/src/main/java/org/killbill/billing/util/security/api/DefaultSecurityApi.java
new file mode 100644
index 0000000..c601350
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/api/DefaultSecurityApi.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.api;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.subject.Subject;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.security.Logical;
+import org.killbill.billing.security.Permission;
+import org.killbill.billing.security.SecurityApiException;
+import org.killbill.billing.security.api.SecurityApi;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.Lists;
+
+public class DefaultSecurityApi implements SecurityApi {
+
+ private static final String[] allPermissions = new String[Permission.values().length];
+
+ @Override
+ public Set<Permission> getCurrentUserPermissions(final TenantContext context) {
+ final Permission[] killbillPermissions = Permission.values();
+ final String[] killbillPermissionsString = getAllPermissionsAsStrings();
+
+ final Subject subject = SecurityUtils.getSubject();
+ // Bulk (optimized) call
+ final boolean[] permissions = subject.isPermitted(killbillPermissionsString);
+
+ final Set<Permission> userPermissions = new HashSet<Permission>();
+ for (int i = 0; i < permissions.length; i++) {
+ if (permissions[i]) {
+ userPermissions.add(killbillPermissions[i]);
+ }
+ }
+
+ return userPermissions;
+ }
+
+ @Override
+ public void checkCurrentUserPermissions(final List<Permission> permissions, final Logical logical, final TenantContext context) throws SecurityApiException {
+ final String[] permissionsString = Lists.<Permission, String>transform(permissions, Functions.toStringFunction()).toArray(new String[permissions.size()]);
+
+ try {
+ final Subject subject = SecurityUtils.getSubject();
+ if (permissionsString.length == 1) {
+ subject.checkPermission(permissionsString[0]);
+ } else if (Logical.AND.equals(logical)) {
+ subject.checkPermissions(permissionsString);
+ } else if (Logical.OR.equals(logical)) {
+ boolean hasAtLeastOnePermission = false;
+ for (final String permission : permissionsString) {
+ if (subject.isPermitted(permission)) {
+ hasAtLeastOnePermission = true;
+ break;
+ }
+ }
+
+ // Cause the exception if none match
+ if (!hasAtLeastOnePermission) {
+ subject.checkPermission(permissionsString[0]);
+ }
+ }
+ } catch (AuthorizationException e) {
+ throw new SecurityApiException(e, ErrorCode.SECURITY_NOT_ENOUGH_PERMISSIONS);
+ }
+ }
+
+ private String[] getAllPermissionsAsStrings() {
+ if (allPermissions[0] == null) {
+ synchronized (allPermissions) {
+ if (allPermissions[0] == null) {
+ final Permission[] killbillPermissions = Permission.values();
+ for (int i = 0; i < killbillPermissions.length; i++) {
+ allPermissions[i] = killbillPermissions[i].toString();
+ }
+ }
+ }
+ }
+
+ return allPermissions;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/api/DefaultSecurityService.java b/util/src/main/java/org/killbill/billing/util/security/api/DefaultSecurityService.java
new file mode 100644
index 0000000..14fa2a0
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/api/DefaultSecurityService.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.api;
+
+import javax.inject.Inject;
+
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.mgt.SecurityManager;
+
+import org.killbill.billing.lifecycle.LifecycleHandlerType;
+import org.killbill.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+
+public class DefaultSecurityService implements SecurityService {
+
+ public static final String SECURITY_SERVICE_NAME = "security-service";
+
+ private final SecurityManager securityManager;
+
+ @Inject
+ public DefaultSecurityService(final SecurityManager securityManager) {
+ this.securityManager = securityManager;
+ }
+
+ @Override
+ public String getName() {
+ return SECURITY_SERVICE_NAME;
+ }
+
+ @LifecycleHandlerType(LifecycleHandlerType.LifecycleLevel.INIT_SERVICE)
+ public void initialize() {
+ SecurityUtils.setSecurityManager(securityManager);
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE)
+ public void stop() {
+ SecurityUtils.setSecurityManager(null);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/api/SecurityService.java b/util/src/main/java/org/killbill/billing/util/security/api/SecurityService.java
new file mode 100644
index 0000000..96c4905
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/api/SecurityService.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.api;
+
+import org.killbill.billing.lifecycle.KillbillService;
+
+public interface SecurityService extends KillbillService {
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/PermissionAnnotationHandler.java b/util/src/main/java/org/killbill/billing/util/security/PermissionAnnotationHandler.java
new file mode 100644
index 0000000..647b598
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/PermissionAnnotationHandler.java
@@ -0,0 +1,67 @@
+package org.killbill.billing.util.security;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+import java.lang.annotation.Annotation;
+
+import javax.inject.Inject;
+
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.aop.AuthorizingAnnotationHandler;
+
+import org.killbill.billing.security.Permission;
+import org.killbill.billing.security.RequiresPermissions;
+import org.killbill.billing.security.SecurityApiException;
+import org.killbill.billing.security.api.SecurityApi;
+import org.killbill.billing.callcontext.DefaultTenantContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+
+import com.google.common.collect.ImmutableList;
+
+public class PermissionAnnotationHandler extends AuthorizingAnnotationHandler {
+
+ private final TenantContext context = new DefaultTenantContext(null);
+
+ @Inject
+ SecurityApi securityApi;
+
+ public PermissionAnnotationHandler() {
+ super(RequiresPermissions.class);
+ }
+
+ public void assertAuthorized(final Annotation annotation) throws AuthorizationException {
+ if (!(annotation instanceof RequiresPermissions)) {
+ return;
+ }
+
+ final RequiresPermissions requiresPermissions = (RequiresPermissions) annotation;
+ try {
+ securityApi.checkCurrentUserPermissions(ImmutableList.<Permission>copyOf(requiresPermissions.value()), requiresPermissions.logical(), context);
+ } catch (SecurityApiException e) {
+ if (e.getCause() != null && e.getCause() instanceof AuthorizationException) {
+ throw (AuthorizationException) e.getCause();
+ } else if (e.getCause() != null) {
+ throw new AuthorizationException(e.getCause());
+ } else {
+ throw new AuthorizationException(e);
+ }
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/PermissionAnnotationMethodInterceptor.java b/util/src/main/java/org/killbill/billing/util/security/PermissionAnnotationMethodInterceptor.java
new file mode 100644
index 0000000..f8bdf71
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/PermissionAnnotationMethodInterceptor.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security;
+
+import org.apache.shiro.aop.AnnotationResolver;
+import org.apache.shiro.authz.aop.AuthorizingAnnotationHandler;
+import org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor;
+
+public class PermissionAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {
+
+ public PermissionAnnotationMethodInterceptor(final AuthorizingAnnotationHandler handler, final AnnotationResolver resolver) {
+ super(handler, resolver);
+ }
+}
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
new file mode 100644
index 0000000..dae7fc2
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionDao.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.dao;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+import javax.inject.Inject;
+
+import org.apache.shiro.session.Session;
+import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.Transaction;
+import org.skife.jdbi.v2.TransactionStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapperFactory;
+
+public class JDBCSessionDao extends CachingSessionDAO {
+
+ private static final Logger log = LoggerFactory.getLogger(JDBCSessionDao.class);
+
+ private JDBCSessionSqlDao jdbcSessionSqlDao;
+
+ @Inject
+ public JDBCSessionDao(final IDBI dbi) {
+ if (dbi instanceof DBI) {
+ // TODO PIERRE Move to DBIProvider, once it's in util
+ ((DBI) dbi).registerMapper(new LowerToCamelBeanMapperFactory(SessionModelDao.class));
+ }
+ this.jdbcSessionSqlDao = dbi.onDemand(JDBCSessionSqlDao.class);
+ }
+
+ @Override
+ protected void doUpdate(final Session session) {
+ jdbcSessionSqlDao.update(new SessionModelDao(session));
+ }
+
+ @Override
+ protected void doDelete(final Session session) {
+ jdbcSessionSqlDao.delete(new SessionModelDao(session));
+ }
+
+ @Override
+ protected Serializable doCreate(final Session session) {
+ final Serializable sessionId = jdbcSessionSqlDao.inTransaction(new Transaction<Long, JDBCSessionSqlDao>() {
+ @Override
+ public Long inTransaction(final JDBCSessionSqlDao transactional, final TransactionStatus status) throws Exception {
+ transactional.create(new SessionModelDao(session));
+ return transactional.getLastInsertId();
+ }
+ });
+ assignSessionId(session, sessionId);
+ return sessionId;
+ }
+
+ @Override
+ protected Session doReadSession(final Serializable sessionId) {
+ final SessionModelDao sessionModelDao = jdbcSessionSqlDao.read(sessionId);
+ if (sessionModelDao == null) {
+ return null;
+ }
+
+ try {
+ return sessionModelDao.toSimpleSession();
+ } catch (IOException e) {
+ log.warn("Corrupted cookie", e);
+ return null;
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.java
new file mode 100644
index 0000000..5987b92
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.dao;
+
+import java.io.Serializable;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
+
+import org.killbill.commons.jdbi.binder.SmartBindBean;
+
+@UseStringTemplate3StatementLocator
+public interface JDBCSessionSqlDao extends Transactional<JDBCSessionSqlDao> {
+
+ @SqlQuery
+ public SessionModelDao read(@Bind("recordId") final Serializable sessionId);
+
+ @SqlUpdate
+ public void create(@SmartBindBean final SessionModelDao sessionModelDao);
+
+ @SqlUpdate
+ public void update(@SmartBindBean final SessionModelDao sessionModelDao);
+
+ @SqlUpdate
+ public void delete(@SmartBindBean final SessionModelDao sessionModelDao);
+
+ @SqlQuery
+ public Long getLastInsertId();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/SessionModelDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/SessionModelDao.java
new file mode 100644
index 0000000..3b5e3f0
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/SessionModelDao.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.dao;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.shiro.io.DefaultSerializer;
+import org.apache.shiro.io.Serializer;
+import org.apache.shiro.session.Session;
+import org.apache.shiro.session.mgt.SimpleSession;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+public class SessionModelDao {
+
+ private final Serializer<Map> serializer = new DefaultSerializer<Map>();
+
+ private Long recordId;
+ private DateTime startTimestamp;
+ private DateTime lastAccessTime;
+ private long timeout;
+ private String host;
+ private byte[] sessionData;
+
+ public SessionModelDao() { /* For the DAO mapper */ }
+
+ public SessionModelDao(final Session session) {
+ this.recordId = (Long) session.getId();
+ this.startTimestamp = new DateTime(session.getStartTimestamp(), DateTimeZone.UTC);
+ this.lastAccessTime = new DateTime(session.getLastAccessTime(), DateTimeZone.UTC);
+ this.timeout = session.getTimeout();
+ this.host = session.getHost();
+ try {
+ this.sessionData = serializeSessionData(session);
+ } catch (IOException e) {
+ this.sessionData = new byte[]{};
+ }
+ }
+
+ public Session toSimpleSession() throws IOException {
+ final SimpleSession simpleSession = new SimpleSession();
+ simpleSession.setId(recordId);
+ simpleSession.setStartTimestamp(startTimestamp.toDate());
+ simpleSession.setLastAccessTime(lastAccessTime.toDate());
+ simpleSession.setTimeout(timeout);
+ simpleSession.setHost(host);
+
+ final Map attributes = serializer.deserialize(sessionData);
+ //noinspection unchecked
+ simpleSession.setAttributes(attributes);
+
+ return simpleSession;
+ }
+
+ public Long getRecordId() {
+ return recordId;
+ }
+
+ public DateTime getStartTimestamp() {
+ return startTimestamp;
+ }
+
+ public DateTime getLastAccessTime() {
+ return lastAccessTime;
+ }
+
+ public long getTimeout() {
+ return timeout;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public byte[] getSessionData() {
+ return sessionData;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("SessionModelDao{");
+ sb.append("recordId=").append(recordId);
+ sb.append(", startTimestamp=").append(startTimestamp);
+ sb.append(", lastAccessTime=").append(lastAccessTime);
+ sb.append(", timeout=").append(timeout);
+ sb.append(", host='").append(host).append('\'');
+ sb.append(", sessionData=").append(Arrays.toString(sessionData));
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final SessionModelDao that = (SessionModelDao) o;
+
+ if (timeout != that.timeout) {
+ return false;
+ }
+ if (host != null ? !host.equals(that.host) : that.host != null) {
+ return false;
+ }
+ if (lastAccessTime != null ? !lastAccessTime.equals(that.lastAccessTime) : that.lastAccessTime != null) {
+ return false;
+ }
+ if (recordId != null ? !recordId.equals(that.recordId) : that.recordId != null) {
+ return false;
+ }
+ if (!Arrays.equals(sessionData, that.sessionData)) {
+ return false;
+ }
+ if (startTimestamp != null ? !startTimestamp.equals(that.startTimestamp) : that.startTimestamp != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = recordId != null ? recordId.hashCode() : 0;
+ result = 31 * result + (startTimestamp != null ? startTimestamp.hashCode() : 0);
+ result = 31 * result + (lastAccessTime != null ? lastAccessTime.hashCode() : 0);
+ result = 31 * result + (int) (timeout ^ (timeout >>> 32));
+ result = 31 * result + (host != null ? host.hashCode() : 0);
+ result = 31 * result + (sessionData != null ? Arrays.hashCode(sessionData) : 0);
+ return result;
+ }
+
+ private byte[] serializeSessionData(final Session session) throws IOException {
+ final Map<Object, Object> sessionAttributes = new HashMap<Object, Object>();
+ for (final Object key : session.getAttributeKeys()) {
+ sessionAttributes.put(key, session.getAttribute(key));
+ }
+
+ return serializer.serialize(sessionAttributes);
+ }
+}
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
new file mode 100644
index 0000000..1c7c00f
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.realm;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.LdapContext;
+
+import org.apache.shiro.authc.AuthenticationException;
+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.ldap.JndiLdapContextFactory;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.util.config.SecurityConfig;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+
+public class KillBillJndiLdapRealm extends JndiLdapRealm {
+
+ private static final Logger log = LoggerFactory.getLogger(KillBillJndiLdapRealm.class);
+
+ private static final String USERDN_SUBSTITUTION_TOKEN = "{0}";
+
+ private static final SearchControls SUBTREE_SCOPE = new SearchControls();
+
+ static {
+ SUBTREE_SCOPE.setSearchScope(SearchControls.SUBTREE_SCOPE);
+ }
+
+ private static final Splitter SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults();
+
+ private final String searchBase;
+ private final String groupSearchFilter;
+ private final String groupNameId;
+ private final Map<String, Collection<String>> permissionsByGroup = Maps.newLinkedHashMap();
+
+ @Inject
+ public KillBillJndiLdapRealm(final SecurityConfig securityConfig) {
+ super();
+
+ if (securityConfig.getShiroLDAPUserDnTemplate() != null) {
+ setUserDnTemplate(securityConfig.getShiroLDAPUserDnTemplate());
+ }
+
+ final JndiLdapContextFactory contextFactory = (JndiLdapContextFactory) getContextFactory();
+ if (securityConfig.disableShiroLDAPSSLCheck()) {
+ contextFactory.getEnvironment().put("java.naming.ldap.factory.socket", SkipSSLCheckSocketFactory.class.getName());
+ }
+ if (securityConfig.getShiroLDAPUrl() != null) {
+ contextFactory.setUrl(securityConfig.getShiroLDAPUrl());
+ }
+ if (securityConfig.getShiroLDAPSystemUsername() != null) {
+ contextFactory.setSystemUsername(securityConfig.getShiroLDAPSystemUsername());
+ }
+ if (securityConfig.getShiroLDAPSystemPassword() != null) {
+ contextFactory.setSystemPassword(securityConfig.getShiroLDAPSystemPassword());
+ }
+ if (securityConfig.getShiroLDAPAuthenticationMechanism() != null) {
+ contextFactory.setAuthenticationMechanism(securityConfig.getShiroLDAPAuthenticationMechanism());
+ }
+ setContextFactory(contextFactory);
+
+ searchBase = securityConfig.getShiroLDAPSearchBase();
+ groupSearchFilter = securityConfig.getShiroLDAPGroupSearchFilter();
+ groupNameId = securityConfig.getShiroLDAPGroupNameID();
+
+ if (securityConfig.getShiroLDAPPermissionsByGroup() != null) {
+ final Ini ini = new Ini();
+ // 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)));
+ permissionsByGroup.put(role, permissions);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected AuthorizationInfo queryForAuthorizationInfo(final PrincipalCollection principals, final LdapContextFactory ldapContextFactory) throws NamingException {
+ final Set<String> userGroups = findLDAPGroupsForUser(principals, ldapContextFactory);
+
+ final SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(userGroups);
+ final Set<String> stringPermissions = groupsPermissions(userGroups);
+ simpleAuthorizationInfo.setStringPermissions(stringPermissions);
+
+ return simpleAuthorizationInfo;
+ }
+
+ private Set<String> findLDAPGroupsForUser(final PrincipalCollection principals, final LdapContextFactory ldapContextFactory) throws NamingException {
+ final String username = (String) getAvailablePrincipal(principals);
+
+ LdapContext systemLdapCtx = null;
+ try {
+ systemLdapCtx = ldapContextFactory.getSystemLdapContext();
+ return findLDAPGroupsForUser(username, systemLdapCtx);
+ } catch (AuthenticationException ex) {
+ log.info("LDAP authentication exception: " + ex.getLocalizedMessage());
+ return ImmutableSet.<String>of();
+ } finally {
+ LdapUtils.closeContext(systemLdapCtx);
+ }
+ }
+
+ private Set<String> findLDAPGroupsForUser(final String userName, final LdapContext ldapCtx) throws NamingException {
+ final NamingEnumeration<SearchResult> foundGroups = ldapCtx.search(searchBase,
+ groupSearchFilter.replace(USERDN_SUBSTITUTION_TOKEN, userName),
+ SUBTREE_SCOPE);
+
+ // 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);
+ }
+
+ private String extractGroupNameFromSearchResult(final SearchResult searchResult) {
+ // Get all attributes for that group
+ final Iterator<? extends Attribute> attributesIterator = Iterators.forEnumeration(searchResult.getAttributes().getAll());
+
+ // Find the attribute representing the group name
+ final Iterator<? extends Attribute> groupNameAttributesIterator = Iterators.filter(attributesIterator,
+ new Predicate<Attribute>() {
+ @Override
+ public boolean apply(final Attribute attribute) {
+ return groupNameId.equalsIgnoreCase(attribute.getID());
+ }
+ });
+
+ // 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>() {
+ @Override
+ public String 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 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;
+ }
+ }
+
+ private Set<String> groupsPermissions(final Set<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;
+ }
+
+ @VisibleForTesting
+ public Map<String, Collection<String>> getPermissionsByGroup() {
+ return permissionsByGroup;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/SkipSSLCheckSocketFactory.java b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/SkipSSLCheckSocketFactory.java
new file mode 100644
index 0000000..d1bd943
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/SkipSSLCheckSocketFactory.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.realm;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SkipSSLCheckSocketFactory extends SocketFactory {
+
+ private static final Logger log = LoggerFactory.getLogger(SkipSSLCheckSocketFactory.class);
+
+ private static SocketFactory skipSSLCheckFactory = null;
+
+ static {
+ final TrustManager[] noOpTrustManagers = new TrustManager[]{
+ new X509TrustManager() {
+ public X509Certificate[] getAcceptedIssuers() { return null; }
+
+ public void checkClientTrusted(X509Certificate[] c, String a) { }
+
+ public void checkServerTrusted(X509Certificate[] c, String a) { }
+ }};
+
+ try {
+ final SSLContext context = SSLContext.getInstance("SSL");
+ context.init(null, noOpTrustManagers, new SecureRandom());
+ skipSSLCheckFactory = context.getSocketFactory();
+ } catch (GeneralSecurityException e) {
+ log.warn("SSL exception", e);
+ }
+ }
+
+ public static SocketFactory getDefault() {
+ return new SkipSSLCheckSocketFactory();
+ }
+
+ public Socket createSocket(final String arg0, final int arg1) throws IOException {
+ return skipSSLCheckFactory.createSocket(arg0, arg1);
+ }
+
+ public Socket createSocket(final InetAddress arg0, final int arg1) throws IOException {
+ return skipSSLCheckFactory.createSocket(arg0, arg1);
+ }
+
+ public Socket createSocket(final String arg0, final int arg1, final InetAddress arg2, final int arg3) throws IOException {
+ return skipSSLCheckFactory.createSocket(arg0, arg1, arg2, arg3);
+ }
+
+ public Socket createSocket(final InetAddress arg0, final int arg1, final InetAddress arg2, final int arg3) throws IOException {
+ return skipSSLCheckFactory.createSocket(arg0, arg1, arg2, arg3);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/svcsapi/bus/BusService.java b/util/src/main/java/org/killbill/billing/util/svcsapi/bus/BusService.java
new file mode 100644
index 0000000..174e0b4
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/svcsapi/bus/BusService.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.svcsapi.bus;
+
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.lifecycle.KillbillService;
+
+public interface BusService extends KillbillService {
+ public PersistentBus getBus();
+}
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
new file mode 100644
index 0000000..7e10d0f
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/DefaultTagUserApi.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.DefaultControlTag;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.DescriptiveTag;
+import org.killbill.billing.util.tag.Tag;
+import org.killbill.billing.util.tag.TagDefinition;
+import org.killbill.billing.util.tag.dao.TagDao;
+import org.killbill.billing.util.tag.dao.TagDefinitionDao;
+import org.killbill.billing.util.tag.dao.TagDefinitionModelDao;
+import org.killbill.billing.util.tag.dao.TagModelDao;
+import org.killbill.billing.util.tag.dao.TagModelDaoHelper;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;
+
+public class DefaultTagUserApi implements TagUserApi {
+
+ private static final Function<TagModelDao, Tag> TAG_MODEL_DAO_TAG_FUNCTION = new Function<TagModelDao, Tag>() {
+ @Override
+ public Tag apply(final TagModelDao input) {
+ return TagModelDaoHelper.isControlTag(input.getTagDefinitionId()) ?
+ new DefaultControlTag(input.getId(), ControlTagType.getTypeFromId(input.getTagDefinitionId()), input.getObjectType(), input.getObjectId(), input.getCreatedDate()) :
+ new DescriptiveTag(input.getId(), input.getTagDefinitionId(), input.getObjectType(), input.getObjectId(), input.getCreatedDate());
+ }
+ };
+
+ private final InternalCallContextFactory internalCallContextFactory;
+ private final TagDefinitionDao tagDefinitionDao;
+ private final TagDao tagDao;
+
+ @Inject
+ public DefaultTagUserApi(final InternalCallContextFactory internalCallContextFactory, final TagDefinitionDao tagDefinitionDao, final TagDao tagDao) {
+ this.internalCallContextFactory = internalCallContextFactory;
+ this.tagDefinitionDao = tagDefinitionDao;
+ this.tagDao = tagDao;
+ }
+
+ @Override
+ public List<TagDefinition> getTagDefinitions(final TenantContext context) {
+ return ImmutableList.<TagDefinition>copyOf(Collections2.transform(tagDefinitionDao.getTagDefinitions(internalCallContextFactory.createInternalTenantContext(context)),
+ new Function<TagDefinitionModelDao, TagDefinition>() {
+ @Override
+ public TagDefinition apply(final TagDefinitionModelDao input) {
+ return new DefaultTagDefinition(input, TagModelDaoHelper.isControlTag(input.getName()));
+ }
+ }));
+ }
+
+ @Override
+ public TagDefinition createTagDefinition(final String definitionName, final String description, final CallContext context) throws TagDefinitionApiException {
+ final TagDefinitionModelDao tagDefinitionModelDao = tagDefinitionDao.create(definitionName, description, internalCallContextFactory.createInternalCallContext(context));
+ return new DefaultTagDefinition(tagDefinitionModelDao, TagModelDaoHelper.isControlTag(tagDefinitionModelDao.getName()));
+ }
+
+ @Override
+ public void deleteTagDefinition(final UUID definitionId, final CallContext context) throws TagDefinitionApiException {
+ tagDefinitionDao.deleteById(definitionId, internalCallContextFactory.createInternalCallContext(context));
+ }
+
+ @Override
+ public TagDefinition getTagDefinition(final UUID tagDefinitionId, final TenantContext context)
+ throws TagDefinitionApiException {
+ final TagDefinitionModelDao tagDefinitionModelDao = tagDefinitionDao.getById(tagDefinitionId, internalCallContextFactory.createInternalTenantContext(context));
+ return new DefaultTagDefinition(tagDefinitionModelDao, TagModelDaoHelper.isControlTag(tagDefinitionModelDao.getName()));
+ }
+
+ @Override
+ public List<TagDefinition> getTagDefinitions(final Collection<UUID> tagDefinitionIds, final TenantContext context)
+ throws TagDefinitionApiException {
+ return ImmutableList.<TagDefinition>copyOf(Collections2.transform(tagDefinitionDao.getByIds(tagDefinitionIds, internalCallContextFactory.createInternalTenantContext(context)),
+ new Function<TagDefinitionModelDao, TagDefinition>() {
+ @Override
+ public TagDefinition apply(final TagDefinitionModelDao input) {
+ return new DefaultTagDefinition(input, TagModelDaoHelper.isControlTag(input.getName()));
+ }
+ }));
+ }
+
+ @Override
+ public void addTags(final UUID objectId, final ObjectType objectType, final Collection<UUID> tagDefinitionIds, final CallContext context) throws TagApiException {
+ for (final UUID tagDefinitionId : tagDefinitionIds) {
+ addTag(objectId, objectType, tagDefinitionId, context);
+ }
+ }
+
+ @Override
+ public void addTag(final UUID objectId, final ObjectType objectType, final UUID tagDefinitionId, final CallContext context) throws TagApiException {
+ final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(objectId, objectType, context);
+ final TagModelDao tag = new TagModelDao(context.getCreatedDate(), tagDefinitionId, objectId, objectType);
+ try {
+ tagDao.create(tag, internalContext);
+ } catch (TagApiException e) {
+ // Be lenient here and make the addTag method idempotent
+ if (ErrorCode.TAG_ALREADY_EXISTS.getCode() != e.getCode()) {
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public void removeTag(final UUID objectId, final ObjectType objectType, final UUID tagDefinitionId, final CallContext context) throws TagApiException {
+ tagDao.deleteTag(objectId, objectType, tagDefinitionId, internalCallContextFactory.createInternalCallContext(objectId, objectType, context));
+ }
+
+ @Override
+ public Pagination<Tag> searchTags(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
+ return getEntityPaginationNoException(limit,
+ new SourcePaginationBuilder<TagModelDao, TagApiException>() {
+ @Override
+ public Pagination<TagModelDao> build() {
+ return tagDao.searchTags(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+ }
+ },
+ TAG_MODEL_DAO_TAG_FUNCTION);
+ }
+
+ @Override
+ public Pagination<Tag> getTags(final Long offset, final Long limit, final TenantContext context) {
+ return getEntityPaginationNoException(limit,
+ new SourcePaginationBuilder<TagModelDao, TagApiException>() {
+ @Override
+ public Pagination<TagModelDao> build() {
+ return tagDao.get(offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+ }
+ },
+ TAG_MODEL_DAO_TAG_FUNCTION);
+ }
+
+ @Override
+ public void removeTags(final UUID objectId, final ObjectType objectType, final Collection<UUID> tagDefinitionIds, final CallContext context) throws TagApiException {
+ // TODO: consider making this batch
+ for (final UUID tagDefinitionId : tagDefinitionIds) {
+ tagDao.deleteTag(objectId, objectType, tagDefinitionId, internalCallContextFactory.createInternalCallContext(objectId, objectType, context));
+ }
+ }
+
+ @Override
+ public TagDefinition getTagDefinitionForName(final String tagDefinitionName, final TenantContext context)
+ throws TagDefinitionApiException {
+ return new DefaultTagDefinition(tagDefinitionDao.getByName(tagDefinitionName, internalCallContextFactory.createInternalTenantContext(context)),
+ TagModelDaoHelper.isControlTag(tagDefinitionName));
+ }
+
+ @Override
+ public List<Tag> getTagsForObject(final UUID objectId, final ObjectType objectType, final boolean includedDeleted, final TenantContext context) {
+ return withModelTransform(tagDao.getTagsForObject(objectId, objectType, includedDeleted, internalCallContextFactory.createInternalTenantContext(context)));
+ }
+
+ @Override
+ public List<Tag> getTagsForAccountType(final UUID accountId, final ObjectType objectType, final boolean includedDeleted, final TenantContext context) {
+ return withModelTransform(tagDao.getTagsForAccountType(accountId, objectType, includedDeleted, internalCallContextFactory.createInternalTenantContext(accountId, context)));
+ }
+
+ @Override
+ public List<Tag> getTagsForAccount(final UUID accountId, final boolean includedDeleted, final TenantContext context) {
+ return withModelTransform(tagDao.getTagsForAccount(includedDeleted, internalCallContextFactory.createInternalTenantContext(accountId, context)));
+ }
+
+ private List<Tag> withModelTransform(final Collection<TagModelDao> input) {
+ return ImmutableList.<Tag>copyOf(Collections2.transform(input, TAG_MODEL_DAO_TAG_FUNCTION));
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagCreationEvent.java b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagCreationEvent.java
new file mode 100644
index 0000000..efdff9b
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagCreationEvent.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.ControlTagCreationInternalEvent;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultControlTagCreationEvent extends BusEventBase implements ControlTagCreationInternalEvent {
+
+ private final UUID tagId;
+ private final UUID objectId;
+ private final ObjectType objectType;
+ private final TagDefinition tagDefinition;
+
+ @JsonCreator
+ public DefaultControlTagCreationEvent(@JsonProperty("tagId") final UUID tagId,
+ @JsonProperty("objectId") final UUID objectId,
+ @JsonProperty("objectType") final ObjectType objectType,
+ @JsonProperty("tagDefinition") final TagDefinition tagDefinition,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.tagId = tagId;
+ this.objectId = objectId;
+ this.objectType = objectType;
+ this.tagDefinition = tagDefinition;
+ }
+
+ @Override
+ public UUID getTagId() {
+ return tagId;
+ }
+
+ @Override
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ @Override
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ @Override
+ public TagDefinition getTagDefinition() {
+ return tagDefinition;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.CONTROL_TAG_CREATION;
+ }
+
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultControlTagCreationEvent");
+ sb.append("{objectId=").append(objectId);
+ sb.append(", tagId=").append(tagId);
+ sb.append(", objectType=").append(objectType);
+ sb.append(", tagDefinition=").append(tagDefinition);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultControlTagCreationEvent that = (DefaultControlTagCreationEvent) o;
+
+ if (objectId != null ? !objectId.equals(that.objectId) : that.objectId != null) {
+ return false;
+ }
+ if (objectType != that.objectType) {
+ return false;
+ }
+ if (tagDefinition != null ? !tagDefinition.equals(that.tagDefinition) : that.tagDefinition != null) {
+ return false;
+ }
+ if (tagId != null ? !tagId.equals(that.tagId) : that.tagId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tagId != null ? tagId.hashCode() : 0;
+ result = 31 * result + (objectId != null ? objectId.hashCode() : 0);
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ result = 31 * result + (tagDefinition != null ? tagDefinition.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDefinitionCreationEvent.java b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDefinitionCreationEvent.java
new file mode 100644
index 0000000..f88587e
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDefinitionCreationEvent.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.ControlTagDefinitionCreationInternalEvent;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultControlTagDefinitionCreationEvent extends BusEventBase implements ControlTagDefinitionCreationInternalEvent {
+
+ private final UUID tagDefinitionId;
+ private final TagDefinition tagDefinition;
+
+ @JsonCreator
+ public DefaultControlTagDefinitionCreationEvent(@JsonProperty("tagDefinitionId") final UUID tagDefinitionId,
+ @JsonProperty("tagDefinition") final TagDefinition tagDefinition,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.tagDefinitionId = tagDefinitionId;
+ this.tagDefinition = tagDefinition;
+ }
+
+ @Override
+ public UUID getTagDefinitionId() {
+ return tagDefinitionId;
+ }
+
+ @Override
+ public TagDefinition getTagDefinition() {
+ return tagDefinition;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.CONTROL_TAGDEFINITION_CREATION;
+ }
+
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultControlTagDefinitionCreationEvent");
+ sb.append("{tagDefinition=").append(tagDefinition);
+ sb.append(", tagDefinitionId=").append(tagDefinitionId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultControlTagDefinitionCreationEvent that = (DefaultControlTagDefinitionCreationEvent) o;
+
+ if (tagDefinition != null ? !tagDefinition.equals(that.tagDefinition) : that.tagDefinition != null) {
+ return false;
+ }
+ if (tagDefinitionId != null ? !tagDefinitionId.equals(that.tagDefinitionId) : that.tagDefinitionId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tagDefinitionId != null ? tagDefinitionId.hashCode() : 0;
+ result = 31 * result + (tagDefinition != null ? tagDefinition.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDefinitionDeletionEvent.java b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDefinitionDeletionEvent.java
new file mode 100644
index 0000000..d5f67ec
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDefinitionDeletionEvent.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.ControlTagDefinitionDeletionInternalEvent;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultControlTagDefinitionDeletionEvent extends BusEventBase implements ControlTagDefinitionDeletionInternalEvent {
+
+ private final UUID tagDefinitionId;
+ private final TagDefinition tagDefinition;
+
+ @JsonCreator
+ public DefaultControlTagDefinitionDeletionEvent(@JsonProperty("tagDefinitionId") final UUID tagDefinitionId,
+ @JsonProperty("tagDefinition") final TagDefinition tagDefinition,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.tagDefinitionId = tagDefinitionId;
+ this.tagDefinition = tagDefinition;
+ }
+
+ @Override
+ public UUID getTagDefinitionId() {
+ return tagDefinitionId;
+ }
+
+ @Override
+ public TagDefinition getTagDefinition() {
+ return tagDefinition;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.CONTROL_TAGDEFINITION_DELETION;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultControlTagDefinitionDeletionEvent");
+ sb.append("{tagDefinition=").append(tagDefinition);
+ sb.append(", tagDefinitionId=").append(tagDefinitionId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultControlTagDefinitionDeletionEvent that = (DefaultControlTagDefinitionDeletionEvent) o;
+
+ if (tagDefinition != null ? !tagDefinition.equals(that.tagDefinition) : that.tagDefinition != null) {
+ return false;
+ }
+ if (tagDefinitionId != null ? !tagDefinitionId.equals(that.tagDefinitionId) : that.tagDefinitionId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tagDefinitionId != null ? tagDefinitionId.hashCode() : 0;
+ result = 31 * result + (tagDefinition != null ? tagDefinition.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDeletionEvent.java b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDeletionEvent.java
new file mode 100644
index 0000000..76b73b3
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultControlTagDeletionEvent.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.ControlTagDeletionInternalEvent;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultControlTagDeletionEvent extends BusEventBase implements ControlTagDeletionInternalEvent {
+
+ private final UUID tagId;
+ final UUID objectId;
+ final ObjectType objectType;
+ final TagDefinition tagDefinition;
+
+ @JsonCreator
+ public DefaultControlTagDeletionEvent(@JsonProperty("tagId") final UUID tagId,
+ @JsonProperty("objectId") final UUID objectId,
+ @JsonProperty("objectType") final ObjectType objectType,
+ @JsonProperty("tagDefinition") final TagDefinition tagDefinition,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.tagId = tagId;
+ this.objectId = objectId;
+ this.objectType = objectType;
+ this.tagDefinition = tagDefinition;
+ }
+
+ @Override
+ public UUID getTagId() {
+ return tagId;
+ }
+
+ @Override
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ @Override
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ @Override
+ public TagDefinition getTagDefinition() {
+ return tagDefinition;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.CONTROL_TAG_DELETION;
+ }
+
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultControlTagDeletionEvent");
+ sb.append("{objectId=").append(objectId);
+ sb.append(", tagId=").append(tagId);
+ sb.append(", objectType=").append(objectType);
+ sb.append(", tagDefinition=").append(tagDefinition);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultControlTagDeletionEvent that = (DefaultControlTagDeletionEvent) o;
+
+ if (objectId != null ? !objectId.equals(that.objectId) : that.objectId != null) {
+ return false;
+ }
+ if (objectType != that.objectType) {
+ return false;
+ }
+ if (tagDefinition != null ? !tagDefinition.equals(that.tagDefinition) : that.tagDefinition != null) {
+ return false;
+ }
+ if (tagId != null ? !tagId.equals(that.tagId) : that.tagId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tagId != null ? tagId.hashCode() : 0;
+ result = 31 * result + (objectId != null ? objectId.hashCode() : 0);
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ result = 31 * result + (tagDefinition != null ? tagDefinition.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagCreationEvent.java b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagCreationEvent.java
new file mode 100644
index 0000000..549d325
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagCreationEvent.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.UserTagCreationInternalEvent;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultUserTagCreationEvent extends BusEventBase implements UserTagCreationInternalEvent {
+
+ private final UUID tagId;
+ private final UUID objectId;
+ private final ObjectType objectType;
+ private final TagDefinition tagDefinition;
+
+ @JsonCreator
+ public DefaultUserTagCreationEvent(@JsonProperty("tagId") final UUID tagId,
+ @JsonProperty("objectId") final UUID objectId,
+ @JsonProperty("objectType") final ObjectType objectType,
+ @JsonProperty("tagDefinition") final TagDefinition tagDefinition,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.tagId = tagId;
+ this.objectId = objectId;
+ this.objectType = objectType;
+ this.tagDefinition = tagDefinition;
+ }
+
+ @Override
+ public UUID getTagId() {
+ return tagId;
+ }
+
+ @Override
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ @Override
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ @Override
+ public TagDefinition getTagDefinition() {
+ return tagDefinition;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.USER_TAG_CREATION;
+ }
+
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultUserTagCreationEvent");
+ sb.append("{objectId=").append(objectId);
+ sb.append(", tagId=").append(tagId);
+ sb.append(", objectType=").append(objectType);
+ sb.append(", tagDefinition=").append(tagDefinition);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultUserTagCreationEvent that = (DefaultUserTagCreationEvent) o;
+
+ if (objectId != null ? !objectId.equals(that.objectId) : that.objectId != null) {
+ return false;
+ }
+ if (objectType != that.objectType) {
+ return false;
+ }
+ if (tagDefinition != null ? !tagDefinition.equals(that.tagDefinition) : that.tagDefinition != null) {
+ return false;
+ }
+ if (tagId != null ? !tagId.equals(that.tagId) : that.tagId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tagId != null ? tagId.hashCode() : 0;
+ result = 31 * result + (objectId != null ? objectId.hashCode() : 0);
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ result = 31 * result + (tagDefinition != null ? tagDefinition.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDefinitionCreationEvent.java b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDefinitionCreationEvent.java
new file mode 100644
index 0000000..c88f24a
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDefinitionCreationEvent.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.UserTagDefinitionCreationInternalEvent;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultUserTagDefinitionCreationEvent extends BusEventBase implements UserTagDefinitionCreationInternalEvent {
+
+ private final UUID tagDefinitionId;
+ private final TagDefinition tagDefinition;
+
+ @JsonCreator
+ public DefaultUserTagDefinitionCreationEvent(@JsonProperty("tagDefinitionId") final UUID tagDefinitionId,
+ @JsonProperty("tagDefinition") final TagDefinition tagDefinition,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.tagDefinitionId = tagDefinitionId;
+ this.tagDefinition = tagDefinition;
+ }
+
+ @Override
+ public UUID getTagDefinitionId() {
+ return tagDefinitionId;
+ }
+
+ @Override
+ public TagDefinition getTagDefinition() {
+ return tagDefinition;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.USER_TAGDEFINITION_CREATION;
+ }
+
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultUserTagDefinitionCreationEvent");
+ sb.append("{tagDefinition=").append(tagDefinition);
+ sb.append(", tagDefinitionId=").append(tagDefinitionId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultUserTagDefinitionCreationEvent that = (DefaultUserTagDefinitionCreationEvent) o;
+
+ if (tagDefinition != null ? !tagDefinition.equals(that.tagDefinition) : that.tagDefinition != null) {
+ return false;
+ }
+ if (tagDefinitionId != null ? !tagDefinitionId.equals(that.tagDefinitionId) : that.tagDefinitionId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tagDefinitionId != null ? tagDefinitionId.hashCode() : 0;
+ result = 31 * result + (tagDefinition != null ? tagDefinition.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDefinitionDeletionEvent.java b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDefinitionDeletionEvent.java
new file mode 100644
index 0000000..2c04803
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDefinitionDeletionEvent.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.UserTagDefinitionDeletionInternalEvent;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultUserTagDefinitionDeletionEvent extends BusEventBase implements UserTagDefinitionDeletionInternalEvent {
+
+ private final UUID tagDefinitionId;
+ private final TagDefinition tagDefinition;
+
+ @JsonCreator
+ public DefaultUserTagDefinitionDeletionEvent(@JsonProperty("tagDefinitionId") final UUID tagDefinitionId,
+ @JsonProperty("tagDefinition") final TagDefinition tagDefinition,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.tagDefinitionId = tagDefinitionId;
+ this.tagDefinition = tagDefinition;
+ }
+
+ @Override
+ public UUID getTagDefinitionId() {
+ return tagDefinitionId;
+ }
+
+ @Override
+ public TagDefinition getTagDefinition() {
+ return tagDefinition;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.USER_TAGDEFINITION_DELETION;
+ }
+
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultUserTagDefinitionDeletionEvent");
+ sb.append("{tagDefinition=").append(tagDefinition);
+ sb.append(", tagDefinitionId=").append(tagDefinitionId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultUserTagDefinitionDeletionEvent that = (DefaultUserTagDefinitionDeletionEvent) o;
+
+ if (tagDefinition != null ? !tagDefinition.equals(that.tagDefinition) : that.tagDefinition != null) {
+ return false;
+ }
+ if (tagDefinitionId != null ? !tagDefinitionId.equals(that.tagDefinitionId) : that.tagDefinitionId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tagDefinitionId != null ? tagDefinitionId.hashCode() : 0;
+ result = 31 * result + (tagDefinition != null ? tagDefinition.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDeletionEvent.java b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDeletionEvent.java
new file mode 100644
index 0000000..a2386c2
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/user/DefaultUserTagDeletionEvent.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.UserTagDeletionInternalEvent;
+import org.killbill.billing.util.tag.TagDefinition;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DefaultUserTagDeletionEvent extends BusEventBase implements UserTagDeletionInternalEvent {
+ private final UUID tagId;
+ private final UUID objectId;
+ private final ObjectType objectType;
+ private final TagDefinition tagDefinition;
+
+ @JsonCreator
+ public DefaultUserTagDeletionEvent(@JsonProperty("tagId") final UUID tagId,
+ @JsonProperty("objectId") final UUID objectId,
+ @JsonProperty("objectType") final ObjectType objectType,
+ @JsonProperty("tagDefinition") final TagDefinition tagDefinition,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.tagId = tagId;
+ this.objectId = objectId;
+ this.objectType = objectType;
+ this.tagDefinition = tagDefinition;
+ }
+
+ @Override
+ public UUID getTagId() {
+ return tagId;
+ }
+
+ @Override
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ @Override
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ @Override
+ public TagDefinition getTagDefinition() {
+ return tagDefinition;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.USER_TAG_DELETION;
+ }
+
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultUserTagDeletionEvent");
+ sb.append("{objectId=").append(objectId);
+ sb.append(", tagId=").append(tagId);
+ sb.append(", objectType=").append(objectType);
+ sb.append(", tagDefinition=").append(tagDefinition);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultUserTagDeletionEvent that = (DefaultUserTagDeletionEvent) o;
+
+ if (objectId != null ? !objectId.equals(that.objectId) : that.objectId != null) {
+ return false;
+ }
+ if (objectType != that.objectType) {
+ return false;
+ }
+ if (tagDefinition != null ? !tagDefinition.equals(that.tagDefinition) : that.tagDefinition != null) {
+ return false;
+ }
+ if (tagId != null ? !tagId.equals(that.tagId) : that.tagId != null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tagId != null ? tagId.hashCode() : 0;
+ result = 31 * result + (objectId != null ? objectId.hashCode() : 0);
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ result = 31 * result + (tagDefinition != null ? tagDefinition.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/user/TagEventBuilder.java b/util/src/main/java/org/killbill/billing/util/tag/api/user/TagEventBuilder.java
new file mode 100644
index 0000000..7bdcff4
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/user/TagEventBuilder.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.events.ControlTagCreationInternalEvent;
+import org.killbill.billing.events.ControlTagDefinitionCreationInternalEvent;
+import org.killbill.billing.events.ControlTagDefinitionDeletionInternalEvent;
+import org.killbill.billing.events.ControlTagDeletionInternalEvent;
+import org.killbill.billing.events.UserTagCreationInternalEvent;
+import org.killbill.billing.events.UserTagDefinitionCreationInternalEvent;
+import org.killbill.billing.events.UserTagDefinitionDeletionInternalEvent;
+import org.killbill.billing.events.UserTagDeletionInternalEvent;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.dao.TagDefinitionModelDao;
+
+public class TagEventBuilder {
+
+ public UserTagDefinitionCreationInternalEvent newUserTagDefinitionCreationEvent(final UUID tagDefinitionId, final TagDefinitionModelDao tagDefinition, final Long searchKey1, final Long searchKey2, final UUID userToken) {
+ return new DefaultUserTagDefinitionCreationEvent(tagDefinitionId, new DefaultTagDefinition(tagDefinition, false), searchKey1, searchKey2, userToken);
+ }
+
+ public UserTagDefinitionDeletionInternalEvent newUserTagDefinitionDeletionEvent(final UUID tagDefinitionId, final TagDefinitionModelDao tagDefinition, final Long searchKey1, final Long searchKey2, final UUID userToken) {
+ return new DefaultUserTagDefinitionDeletionEvent(tagDefinitionId, new DefaultTagDefinition(tagDefinition, false), searchKey1, searchKey2, userToken);
+ }
+
+ public ControlTagDefinitionCreationInternalEvent newControlTagDefinitionCreationEvent(final UUID tagDefinitionId, final TagDefinitionModelDao tagDefinition, final Long searchKey1, final Long searchKey2, final UUID userToken) {
+ return new DefaultControlTagDefinitionCreationEvent(tagDefinitionId, new DefaultTagDefinition(tagDefinition, true), searchKey1, searchKey2, userToken);
+ }
+
+ public ControlTagDefinitionDeletionInternalEvent newControlTagDefinitionDeletionEvent(final UUID tagDefinitionId, final TagDefinitionModelDao tagDefinition, final Long searchKey1, final Long searchKey2, final UUID userToken) {
+ return new DefaultControlTagDefinitionDeletionEvent(tagDefinitionId, new DefaultTagDefinition(tagDefinition, true), searchKey1, searchKey2, userToken);
+ }
+
+ public UserTagCreationInternalEvent newUserTagCreationEvent(final UUID tagId, final UUID objectId, final ObjectType objectType, final TagDefinitionModelDao tagDefinition, final Long searchKey1, final Long searchKey2, final UUID userToken) {
+ return new DefaultUserTagCreationEvent(tagId, objectId, objectType, new DefaultTagDefinition(tagDefinition, false), searchKey1, searchKey2, userToken);
+ }
+
+ public UserTagDeletionInternalEvent newUserTagDeletionEvent(final UUID tagId, final UUID objectId, final ObjectType objectType, final TagDefinitionModelDao tagDefinition, final Long searchKey1, final Long searchKey2, final UUID userToken) {
+ return new DefaultUserTagDeletionEvent(tagId, objectId, objectType, new DefaultTagDefinition(tagDefinition, false), searchKey1, searchKey2, userToken);
+ }
+
+ public ControlTagCreationInternalEvent newControlTagCreationEvent(final UUID tagId, final UUID objectId, final ObjectType objectType, final TagDefinitionModelDao tagDefinition, final Long searchKey1, final Long searchKey2, final UUID userToken) {
+ return new DefaultControlTagCreationEvent(tagId, objectId, objectType, new DefaultTagDefinition(tagDefinition, true), searchKey1, searchKey2, userToken);
+ }
+
+ public ControlTagDeletionInternalEvent newControlTagDeletionEvent(final UUID tagId, final UUID objectId, final ObjectType objectType, final TagDefinitionModelDao tagDefinition, final Long searchKey1, final Long searchKey2, final UUID userToken) {
+ return new DefaultControlTagDeletionEvent(tagId, objectId, objectType, new DefaultTagDefinition(tagDefinition, true), searchKey1, searchKey2, userToken);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java
new file mode 100644
index 0000000..4e71f1b
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.IDBI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.events.TagInternalEvent;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
+import org.killbill.billing.util.entity.dao.EntityDaoBase;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.Tag;
+import org.killbill.billing.util.tag.api.user.TagEventBuilder;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+public class DefaultTagDao extends EntityDaoBase<TagModelDao, Tag, TagApiException> implements TagDao {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultTagDao.class);
+
+ private final TagEventBuilder tagEventBuilder;
+ private final PersistentBus bus;
+
+ @Inject
+ public DefaultTagDao(final IDBI dbi, final TagEventBuilder tagEventBuilder, final PersistentBus bus, final Clock clock,
+ final CacheControllerDispatcher controllerDispatcher, final NonEntityDao nonEntityDao) {
+ super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, controllerDispatcher, nonEntityDao), TagSqlDao.class);
+ this.tagEventBuilder = tagEventBuilder;
+ this.bus = bus;
+ }
+
+ @Override
+ public List<TagModelDao> getTagsForObject(final UUID objectId, final ObjectType objectType, final boolean includedDeleted, final InternalTenantContext internalTenantContext) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<TagModelDao>>() {
+ @Override
+ public List<TagModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final TagSqlDao tagSqlDao = entitySqlDaoWrapperFactory.become(TagSqlDao.class);
+ if (includedDeleted) {
+ return tagSqlDao.getTagsForObjectIncludedDeleted(objectId, objectType, internalTenantContext);
+ } else {
+ return tagSqlDao.getTagsForObject(objectId, objectType, internalTenantContext);
+ }
+ }
+ });
+ }
+
+ @Override
+ public List<TagModelDao> getTagsForAccountType(final UUID accountId, final ObjectType objectType, final boolean includedDeleted, final InternalTenantContext internalTenantContext) {
+ final List<TagModelDao> allTags = getTagsForAccount(includedDeleted, internalTenantContext);
+ return ImmutableList.<TagModelDao>copyOf(Collections2.filter(allTags, new Predicate<TagModelDao>() {
+ @Override
+ public boolean apply(final TagModelDao input) {
+ return input.getObjectType() == objectType;
+ }
+ }));
+ }
+
+ @Override
+ public List<TagModelDao> getTagsForAccount(final boolean includedDeleted, final InternalTenantContext internalTenantContext) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<TagModelDao>>() {
+ @Override
+ public List<TagModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final TagSqlDao tagSqlDao = entitySqlDaoWrapperFactory.become(TagSqlDao.class);
+ if (includedDeleted) {
+ return tagSqlDao.getByAccountRecordIdIncludedDeleted(internalTenantContext);
+ } else {
+ return tagSqlDao.getByAccountRecordId(internalTenantContext);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void postBusEventFromTransaction(final TagModelDao tag, final TagModelDao savedTag, final ChangeType changeType,
+ final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final InternalCallContext context)
+ throws BillingExceptionBase {
+
+ final TagInternalEvent tagEvent;
+ final TagDefinitionModelDao tagDefinition = getTagDefinitionFromTransaction(tag.getTagDefinitionId(), entitySqlDaoWrapperFactory, context);
+ final boolean isControlTag = ControlTagType.getTypeFromId(tagDefinition.getId()) != null;
+ switch (changeType) {
+ case INSERT:
+ tagEvent = (isControlTag) ?
+ tagEventBuilder.newControlTagCreationEvent(tag.getId(), tag.getObjectId(), tag.getObjectType(), tagDefinition,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()) :
+ tagEventBuilder.newUserTagCreationEvent(tag.getId(), tag.getObjectId(), tag.getObjectType(), tagDefinition,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+ break;
+ case DELETE:
+ tagEvent = (isControlTag) ?
+ tagEventBuilder.newControlTagDeletionEvent(tag.getId(), tag.getObjectId(), tag.getObjectType(), tagDefinition,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()) :
+ tagEventBuilder.newUserTagDeletionEvent(tag.getId(), tag.getObjectId(), tag.getObjectType(), tagDefinition,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+ break;
+ default:
+ return;
+ }
+
+ try {
+ bus.postFromTransaction(tagEvent, entitySqlDaoWrapperFactory.getSqlDao());
+ } catch (PersistentBus.EventBusException e) {
+ log.warn("Failed to post tag event for tag " + tag.getId().toString(), e);
+ }
+ }
+
+ @Override
+ protected boolean checkEntityAlreadyExists(final EntitySqlDao<TagModelDao, Tag> transactional, final TagModelDao entity, final InternalCallContext context) {
+ return Iterables.find(transactional.getByAccountRecordId(context),
+ new Predicate<TagModelDao>() {
+ @Override
+ public boolean apply(final TagModelDao existingTag) {
+ return entity.equals(existingTag) || entity.isSame(existingTag);
+ }
+ },
+ null) != null;
+ }
+
+ @Override
+ protected TagApiException generateAlreadyExistsException(final TagModelDao entity, final InternalCallContext context) {
+ // Print the tag details, not the id here, as we throw this exception when checking if a tag already exists
+ // by using the isSame(TagModelDao) method (see above)
+ return new TagApiException(ErrorCode.TAG_ALREADY_EXISTS, entity.toString());
+ }
+
+ private TagDefinitionModelDao getTagDefinitionFromTransaction(final UUID tagDefinitionId, final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final InternalTenantContext context) throws TagApiException {
+ TagDefinitionModelDao tagDefintion = null;
+ for (final ControlTagType t : ControlTagType.values()) {
+ if (t.getId().equals(tagDefinitionId)) {
+ tagDefintion = new TagDefinitionModelDao(t);
+ break;
+ }
+ }
+ if (tagDefintion == null) {
+ final TagDefinitionSqlDao transTagDefintionSqlDao = entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class);
+ tagDefintion = transTagDefintionSqlDao.getById(tagDefinitionId.toString(), context);
+ }
+
+ if (tagDefintion == null) {
+ throw new TagApiException(ErrorCode.TAG_DEFINITION_DOES_NOT_EXIST, tagDefinitionId);
+ }
+ return tagDefintion;
+ }
+
+ @Override
+ public void create(final TagModelDao entity, final InternalCallContext context) throws TagApiException {
+ transactionalSqlDao.execute(TagApiException.class, getCreateEntitySqlDaoTransactionWrapper(entity, context));
+ }
+
+ @Override
+ public void deleteTag(final UUID objectId, final ObjectType objectType, final UUID tagDefinitionId, final InternalCallContext context) throws TagApiException {
+
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+
+ final TagDefinitionModelDao tagDefinition = getTagDefinitionFromTransaction(tagDefinitionId, entitySqlDaoWrapperFactory, context);
+ final TagSqlDao transactional = entitySqlDaoWrapperFactory.become(TagSqlDao.class);
+ final List<TagModelDao> tags = transactional.getTagsForObject(objectId, objectType, context);
+ TagModelDao tag = null;
+ for (final TagModelDao cur : tags) {
+ if (cur.getTagDefinitionId().equals(tagDefinitionId)) {
+ tag = cur;
+ break;
+ }
+ }
+ if (tag == null) {
+ throw new TagApiException(ErrorCode.TAG_DOES_NOT_EXIST, tagDefinition.getName());
+ }
+ // Delete the tag
+ transactional.markTagAsDeleted(tag.getId().toString(), context);
+
+ postBusEventFromTransaction(tag, tag, ChangeType.DELETE, entitySqlDaoWrapperFactory, context);
+ return null;
+ }
+ });
+
+ }
+
+ @Override
+ public Pagination<TagModelDao> searchTags(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+ return paginationHelper.getPagination(TagSqlDao.class,
+ new PaginationIteratorBuilder<TagModelDao, Tag, TagSqlDao>() {
+ @Override
+ public Long getCount(final TagSqlDao tagSqlDao, final InternalTenantContext context) {
+ return tagSqlDao.getSearchCount(searchKey, String.format("%%%s%%", searchKey), context);
+ }
+
+ @Override
+ public Iterator<TagModelDao> build(final TagSqlDao tagSqlDao, final Long limit, final InternalTenantContext context) {
+ return tagSqlDao.search(searchKey, String.format("%%%s%%", searchKey), offset, limit, context);
+ }
+ },
+ offset,
+ limit,
+ context);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
new file mode 100644
index 0000000..3e75223
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.exceptions.TransactionFailedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.ErrorCode;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.billing.events.TagDefinitionInternalEvent;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.dao.EntityDaoBase;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.TagDefinition;
+import org.killbill.billing.util.tag.api.user.TagEventBuilder;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterators;
+import com.google.inject.Inject;
+
+public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao, TagDefinition, TagDefinitionApiException> implements TagDefinitionDao {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultTagDefinitionDao.class);
+
+ private final TagEventBuilder tagEventBuilder;
+ private final PersistentBus bus;
+
+ @Inject
+ public DefaultTagDefinitionDao(final IDBI dbi, final TagEventBuilder tagEventBuilder, final PersistentBus bus, final Clock clock,
+ final CacheControllerDispatcher controllerDispatcher, final NonEntityDao nonEntityDao) {
+ super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, controllerDispatcher, nonEntityDao), TagDefinitionSqlDao.class);
+ this.tagEventBuilder = tagEventBuilder;
+ this.bus = bus;
+ }
+
+ @Override
+ public List<TagDefinitionModelDao> getTagDefinitions(final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<TagDefinitionModelDao>>() {
+ @Override
+ public List<TagDefinitionModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ // Get user definitions from the database
+ final TagDefinitionSqlDao tagDefinitionSqlDao = entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class);
+ final Iterator<TagDefinitionModelDao> all = tagDefinitionSqlDao.getAll(context);
+ final List<TagDefinitionModelDao> definitionList = new LinkedList<TagDefinitionModelDao>();
+ Iterators.addAll(definitionList, all);
+
+ // Add control tag definitions
+ for (final ControlTagType controlTag : ControlTagType.values()) {
+ definitionList.add(new TagDefinitionModelDao(controlTag));
+ }
+ return definitionList;
+ }
+ });
+ }
+
+ @Override
+ public TagDefinitionModelDao getByName(final String definitionName, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<TagDefinitionModelDao>() {
+ @Override
+ public TagDefinitionModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ for (final ControlTagType controlTag : ControlTagType.values()) {
+ if (controlTag.name().equals(definitionName)) {
+ return new TagDefinitionModelDao(controlTag);
+ }
+ }
+ return entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class).getByName(definitionName, context);
+ }
+ });
+ }
+
+ @Override
+ public TagDefinitionModelDao getById(final UUID definitionId, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<TagDefinitionModelDao>() {
+ @Override
+ public TagDefinitionModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ for (final ControlTagType controlTag : ControlTagType.values()) {
+ if (controlTag.getId().equals(definitionId)) {
+ return new TagDefinitionModelDao(controlTag);
+ }
+ }
+ return entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class).getById(definitionId.toString(), context);
+ }
+ });
+ }
+
+ @Override
+ public List<TagDefinitionModelDao> getByIds(final Collection<UUID> definitionIds, final InternalTenantContext context) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<TagDefinitionModelDao>>() {
+ @Override
+ public List<TagDefinitionModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final List<TagDefinitionModelDao> result = new LinkedList<TagDefinitionModelDao>();
+ for (final UUID cur : definitionIds) {
+ for (final ControlTagType controlTag : ControlTagType.values()) {
+ if (controlTag.getId().equals(cur)) {
+ result.add(new TagDefinitionModelDao(controlTag));
+ break;
+ }
+ }
+ }
+ if (definitionIds.size() > 0) {
+ result.addAll(entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class).getByIds(Collections2.transform(definitionIds, new Function<UUID, String>() {
+ @Override
+ public String apply(final UUID input) {
+ return input.toString();
+ }
+
+ }), context));
+ }
+ return result;
+ }
+ });
+ }
+
+ @Override
+ public TagDefinitionModelDao create(final String definitionName, final String description,
+ final InternalCallContext context) throws TagDefinitionApiException {
+ // Make sure a control tag with this name don't already exist
+ if (TagModelDaoHelper.isControlTag(definitionName)) {
+ throw new TagDefinitionApiException(ErrorCode.TAG_DEFINITION_CONFLICTS_WITH_CONTROL_TAG, definitionName);
+ }
+
+ try {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<TagDefinitionModelDao>() {
+ @Override
+ public TagDefinitionModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final TagDefinitionSqlDao tagDefinitionSqlDao = entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class);
+
+ // Make sure the tag definition doesn't exist already
+ final TagDefinitionModelDao existingDefinition = tagDefinitionSqlDao.getByName(definitionName, context);
+ if (existingDefinition != null) {
+ throw new TagDefinitionApiException(ErrorCode.TAG_DEFINITION_ALREADY_EXISTS, definitionName);
+ }
+
+ // Create it
+ final TagDefinitionModelDao tagDefinition = new TagDefinitionModelDao(context.getCreatedDate(), definitionName, description);
+ tagDefinitionSqlDao.create(tagDefinition, context);
+
+ // Post an event to the bus
+ final boolean isControlTag = TagModelDaoHelper.isControlTag(tagDefinition.getName());
+ final TagDefinitionInternalEvent tagDefinitionEvent;
+ if (isControlTag) {
+ tagDefinitionEvent = tagEventBuilder.newControlTagDefinitionCreationEvent(tagDefinition.getId(), tagDefinition,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+ } else {
+ tagDefinitionEvent = tagEventBuilder.newUserTagDefinitionCreationEvent(tagDefinition.getId(), tagDefinition,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+ }
+ try {
+ bus.postFromTransaction(tagDefinitionEvent, entitySqlDaoWrapperFactory.getSqlDao());
+ } catch (PersistentBus.EventBusException e) {
+ log.warn("Failed to post tag definition creation event for tag " + tagDefinition.getId(), e);
+ }
+
+ return tagDefinition;
+ }
+ });
+ } catch (TransactionFailedException exception) {
+ if (exception.getCause() instanceof TagDefinitionApiException) {
+ throw (TagDefinitionApiException) exception.getCause();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ @Override
+ public void deleteById(final UUID definitionId, final InternalCallContext context) throws TagDefinitionApiException {
+ try {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final TagDefinitionSqlDao tagDefinitionSqlDao = entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class);
+
+ // Make sure the tag definition exists
+ final TagDefinitionModelDao tagDefinition = tagDefinitionSqlDao.getById(definitionId.toString(), context);
+ if (tagDefinition == null) {
+ throw new TagDefinitionApiException(ErrorCode.TAG_DEFINITION_DOES_NOT_EXIST, definitionId);
+ }
+
+ // Make sure it is not used currently
+ if (tagDefinitionSqlDao.tagDefinitionUsageCount(definitionId.toString(), context) > 0) {
+ throw new TagDefinitionApiException(ErrorCode.TAG_DEFINITION_IN_USE, definitionId);
+ }
+
+ // Delete it
+ tagDefinitionSqlDao.markTagDefinitionAsDeleted(definitionId.toString(), context);
+
+ postBusEventFromTransaction(tagDefinition, tagDefinition, ChangeType.DELETE, entitySqlDaoWrapperFactory, context);
+ return null;
+ }
+ });
+ } catch (TransactionFailedException exception) {
+ if (exception.getCause() instanceof TagDefinitionApiException) {
+ throw (TagDefinitionApiException) exception.getCause();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ protected void postBusEventFromTransaction(final TagDefinitionModelDao tagDefinition, final TagDefinitionModelDao savedTagDefinition,
+ final ChangeType changeType, final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final InternalCallContext context)
+ throws BillingExceptionBase {
+
+ final TagDefinitionInternalEvent tagDefinitionEvent;
+ final boolean isControlTag = TagModelDaoHelper.isControlTag(tagDefinition.getName());
+ switch (changeType) {
+ case INSERT:
+ tagDefinitionEvent = (isControlTag) ?
+ tagEventBuilder.newControlTagDefinitionCreationEvent(tagDefinition.getId(), tagDefinition,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()) :
+ tagEventBuilder.newUserTagDefinitionCreationEvent(tagDefinition.getId(), tagDefinition,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+
+ break;
+ case DELETE:
+ tagDefinitionEvent = (isControlTag) ?
+ tagEventBuilder.newControlTagDefinitionDeletionEvent(tagDefinition.getId(), tagDefinition,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()) :
+ tagEventBuilder.newUserTagDefinitionDeletionEvent(tagDefinition.getId(), tagDefinition,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+ break;
+ default:
+ return;
+ }
+
+ try {
+ bus.postFromTransaction(tagDefinitionEvent, entitySqlDaoWrapperFactory.getSqlDao());
+ } catch (PersistentBus.EventBusException e) {
+ log.warn("Failed to post tag definition event for tag " + tagDefinition.getId().toString(), e);
+ }
+ }
+
+ @Override
+ protected TagDefinitionApiException generateAlreadyExistsException(final TagDefinitionModelDao entity, final InternalCallContext context) {
+ return new TagDefinitionApiException(ErrorCode.TAG_DEFINITION_ALREADY_EXISTS, entity.getId());
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/TagDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/TagDao.java
new file mode 100644
index 0000000..e8932a6
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/TagDao.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.EntityDao;
+import org.killbill.billing.util.tag.Tag;
+
+public interface TagDao extends EntityDao<TagModelDao, Tag, TagApiException> {
+
+ void deleteTag(UUID objectId, ObjectType objectType, UUID tagDefinition, InternalCallContext context) throws TagApiException;
+
+ Pagination<TagModelDao> searchTags(String searchKey, Long offset, Long limit, InternalTenantContext context);
+
+ List<TagModelDao> getTagsForObject(UUID objectId, ObjectType objectType, boolean includedDeleted, InternalTenantContext internalTenantContext);
+
+ List<TagModelDao> getTagsForAccountType(UUID accountId, ObjectType objectType, boolean includedDeleted, InternalTenantContext internalTenantContext);
+
+ List<TagModelDao> getTagsForAccount(boolean includedDeleted, InternalTenantContext internalTenantContext);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/TagDefinitionDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/TagDefinitionDao.java
new file mode 100644
index 0000000..cfa2447
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/TagDefinitionDao.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.entity.dao.EntityDao;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public interface TagDefinitionDao extends EntityDao<TagDefinitionModelDao, TagDefinition, TagDefinitionApiException> {
+
+ public List<TagDefinitionModelDao> getTagDefinitions(InternalTenantContext context);
+
+ public TagDefinitionModelDao getByName(String definitionName, InternalTenantContext context);
+
+ public List<TagDefinitionModelDao> getByIds(Collection<UUID> definitionIds, InternalTenantContext context);
+
+ public TagDefinitionModelDao create(String definitionName, String description, InternalCallContext context) throws TagDefinitionApiException;
+
+ public void deleteById(UUID definitionId, InternalCallContext context) throws TagDefinitionApiException;
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/TagDefinitionModelDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/TagDefinitionModelDao.java
new file mode 100644
index 0000000..85d9f49
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/TagDefinitionModelDao.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public class TagDefinitionModelDao extends EntityBase implements EntityModelDao<TagDefinition> {
+
+ private String name;
+ private String description;
+ private Boolean isActive;
+
+ public TagDefinitionModelDao() { /* For the DAO mapper */ }
+
+ public TagDefinitionModelDao(final UUID id, final DateTime createdDate, final DateTime updatedDate, final String name, final String description) {
+ super(id, createdDate, updatedDate);
+ this.name = name;
+ this.description = description;
+ this.isActive = true;
+ }
+
+ public TagDefinitionModelDao(final ControlTagType tag) {
+ this(tag.getId(), null, null, tag.name(), tag.getDescription());
+ }
+
+ public TagDefinitionModelDao(final DateTime createdDate, final String name, final String description) {
+ this(UUID.randomUUID(), createdDate, createdDate, name, description);
+ }
+
+ public TagDefinitionModelDao(final TagDefinition tagDefinition) {
+ this(tagDefinition.getId(), tagDefinition.getCreatedDate(), tagDefinition.getUpdatedDate(), tagDefinition.getName(),
+ tagDefinition.getDescription());
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Boolean getIsActive() {
+ return isActive;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ public void setIsActive(final Boolean isActive) {
+ this.isActive = isActive;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TagDefinitionModelDao");
+ sb.append("{name='").append(name).append('\'');
+ sb.append(", description='").append(description).append('\'');
+ sb.append(", isActive=").append(isActive);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final TagDefinitionModelDao that = (TagDefinitionModelDao) o;
+
+ if (description != null ? !description.equals(that.description) : that.description != null) {
+ return false;
+ }
+ if (isActive != null ? !isActive.equals(that.isActive) : that.isActive != null) {
+ return false;
+ }
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (isActive != null ? isActive.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.TAG_DEFINITIONS;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return TableName.TAG_DEFINITION_HISTORY;
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/TagDefinitionSqlDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/TagDefinitionSqlDao.java
new file mode 100644
index 0000000..076ef0d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/TagDefinitionSqlDao.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+import org.killbill.billing.util.tag.TagDefinition;
+
+@EntitySqlDaoStringTemplate
+public interface TagDefinitionSqlDao extends EntitySqlDao<TagDefinitionModelDao, TagDefinition> {
+
+ @SqlQuery
+ public TagDefinitionModelDao getByName(@Bind("name") final String definitionName,
+ @BindBean final InternalTenantContext context);
+
+ @SqlUpdate
+ @Audited(ChangeType.DELETE)
+ public void markTagDefinitionAsDeleted(@Bind("id") final String definitionId,
+ @BindBean final InternalCallContext context);
+
+ @SqlQuery
+ public int tagDefinitionUsageCount(@Bind("id") final String definitionId,
+ @BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ public List<TagDefinitionModelDao> getByIds(@UUIDCollectionBinder final Collection<String> definitionIds,
+ @BindBean final InternalTenantContext context);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/TagModelDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/TagModelDao.java
new file mode 100644
index 0000000..a3b5353
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/TagModelDao.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+import org.killbill.billing.util.tag.Tag;
+
+public class TagModelDao extends EntityBase implements EntityModelDao<Tag> {
+
+ private UUID tagDefinitionId;
+ private UUID objectId;
+ private ObjectType objectType;
+ private Boolean isActive;
+
+ public TagModelDao() { /* For the DAO mapper */ }
+
+ public TagModelDao(final DateTime createdDate, final UUID tagDefinitionId,
+ final UUID objectId, final ObjectType objectType) {
+ this(UUID.randomUUID(), createdDate, createdDate, tagDefinitionId, objectId, objectType);
+ }
+
+ public TagModelDao(final UUID id, final DateTime createdDate, final DateTime updatedDate, final UUID tagDefinitionId,
+ final UUID objectId, final ObjectType objectType) {
+ super(id, createdDate, updatedDate);
+ this.tagDefinitionId = tagDefinitionId;
+ this.objectId = objectId;
+ this.objectType = objectType;
+ this.isActive = true;
+ }
+
+ public TagModelDao(final Tag tag) {
+ this(tag.getId(), tag.getCreatedDate(), tag.getUpdatedDate(), tag.getTagDefinitionId(), tag.getObjectId(), tag.getObjectType());
+ }
+
+ public UUID getTagDefinitionId() {
+ return tagDefinitionId;
+ }
+
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ public Boolean getIsActive() {
+ return isActive;
+ }
+
+ public void setTagDefinitionId(final UUID tagDefinitionId) {
+ this.tagDefinitionId = tagDefinitionId;
+ }
+
+ public void setObjectId(final UUID objectId) {
+ this.objectId = objectId;
+ }
+
+ public void setObjectType(final ObjectType objectType) {
+ this.objectType = objectType;
+ }
+
+ public void setIsActive(final Boolean isActive) {
+ this.isActive = isActive;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TagModelDao");
+ sb.append("{tagDefinitionId=").append(tagDefinitionId);
+ sb.append(", objectId=").append(objectId);
+ sb.append(", objectType=").append(objectType);
+ sb.append(", isActive=").append(isActive);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final TagModelDao that = (TagModelDao) o;
+
+ return isSame(that);
+ }
+
+ public boolean isSame(final TagModelDao that) {
+ if (isActive != null ? !isActive.equals(that.isActive) : that.isActive != null) {
+ return false;
+ }
+ if (objectId != null ? !objectId.equals(that.objectId) : that.objectId != null) {
+ return false;
+ }
+ if (objectType != that.objectType) {
+ return false;
+ }
+ if (tagDefinitionId != null ? !tagDefinitionId.equals(that.tagDefinitionId) : that.tagDefinitionId != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (tagDefinitionId != null ? tagDefinitionId.hashCode() : 0);
+ result = 31 * result + (objectId != null ? objectId.hashCode() : 0);
+ result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+ result = 31 * result + (isActive != null ? isActive.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public TableName getTableName() {
+ return TableName.TAG;
+ }
+
+ @Override
+ public TableName getHistoryTableName() {
+ return TableName.TAG_HISTORY;
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/TagModelDaoHelper.java b/util/src/main/java/org/killbill/billing/util/tag/dao/TagModelDaoHelper.java
new file mode 100644
index 0000000..d99fd7a
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/TagModelDaoHelper.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.UUID;
+
+import org.killbill.billing.util.tag.ControlTagType;
+
+public class TagModelDaoHelper {
+
+ private TagModelDaoHelper() {}
+
+ public static boolean isControlTag(final String definitionName) {
+ for (final ControlTagType controlTagName : ControlTagType.values()) {
+ if (controlTagName.toString().equals(definitionName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static boolean isControlTag(final UUID tagDefinitionId) {
+ for (final ControlTagType controlTag : ControlTagType.values()) {
+ if (controlTag.getId().equals(tagDefinitionId)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/TagSqlDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/TagSqlDao.java
new file mode 100644
index 0000000..d1f0b67
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/TagSqlDao.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+import org.killbill.billing.util.tag.Tag;
+
+@EntitySqlDaoStringTemplate
+public interface TagSqlDao extends EntitySqlDao<TagModelDao, Tag> {
+
+ @SqlUpdate
+ @Audited(ChangeType.DELETE)
+ void markTagAsDeleted(@Bind("id") String tagId,
+ @BindBean InternalCallContext context);
+
+ @SqlQuery
+ List<TagModelDao> getTagsForObject(@Bind("objectId") UUID objectId,
+ @Bind("objectType") ObjectType objectType,
+ @BindBean InternalTenantContext internalTenantContext);
+
+ @SqlQuery
+ List<TagModelDao> getTagsForObjectIncludedDeleted(@Bind("objectId") UUID objectId,
+ @Bind("objectType") ObjectType objectType,
+ @BindBean InternalTenantContext internalTenantContext);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/UUIDCollectionBinder.java b/util/src/main/java/org/killbill/billing/util/tag/dao/UUIDCollectionBinder.java
new file mode 100644
index 0000000..db4df65
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/UUIDCollectionBinder.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.util.tag.dao;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Collection;
+
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+
+
+@BindingAnnotation(UUIDCollectionBinder.UUIDCollectionBinderFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface UUIDCollectionBinder {
+ public static class UUIDCollectionBinderFactory implements BinderFactory {
+ @Override
+ public Binder build(Annotation annotation) {
+ return new Binder<UUIDCollectionBinder, Collection<String>>() {
+
+ @Override
+ public void bind(SQLStatement<?> query, UUIDCollectionBinder bind, Collection<String> ids) {
+ query.define("tag_definition_ids", ids);
+
+ int idx = 0;
+ for (String id : ids) {
+ query.bind("id_" + idx, id);
+ idx++;
+ }
+
+ }
+ };
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/DefaultControlTag.java b/util/src/main/java/org/killbill/billing/util/tag/DefaultControlTag.java
new file mode 100644
index 0000000..273d560
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/DefaultControlTag.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ObjectType;
+
+public class DefaultControlTag extends DescriptiveTag implements ControlTag {
+
+ private final ControlTagType controlTagType;
+
+ // use to create new objects
+ public DefaultControlTag(final ControlTagType controlTagType, final ObjectType objectType, final UUID objectId, final DateTime createdDate) {
+ this(UUID.randomUUID(), controlTagType, objectType, objectId, createdDate);
+ }
+
+ // use to hydrate objects when loaded from the persistence layer
+ public DefaultControlTag(final UUID id, final ControlTagType controlTagType, final ObjectType objectType, final UUID objectId, final DateTime createdDate) {
+ super(id, controlTagType.getId(), objectType, objectId, createdDate);
+ this.controlTagType = controlTagType;
+ }
+
+ @Override
+ public ControlTagType getControlTagType() {
+ return controlTagType;
+ }
+
+ @Override
+ public String toString() {
+ return "DefaultControlTag [controlTagType=" + controlTagType + ", id=" + id + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((controlTagType == null) ? 0
+ : controlTagType.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!super.equals(obj)) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final DefaultControlTag other = (DefaultControlTag) obj;
+ if (controlTagType != other.controlTagType) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java b/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java
new file mode 100644
index 0000000..1815fde
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/DefaultTagDefinition.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.entity.EntityBase;
+import org.killbill.billing.util.tag.dao.TagDefinitionModelDao;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableList;
+
+public class DefaultTagDefinition extends EntityBase implements TagDefinition {
+
+ private final String name;
+ private final String description;
+ private final Boolean controlTag;
+ private final List<ObjectType> applicableObjectTypes;
+
+ public DefaultTagDefinition(final TagDefinitionModelDao tagDefinitionModelDao, final boolean isControlTag) {
+ this(tagDefinitionModelDao.getId(), tagDefinitionModelDao.getName(), tagDefinitionModelDao.getDescription(), isControlTag);
+ }
+
+ public DefaultTagDefinition(final String name, final String description, final Boolean isControlTag) {
+ this(UUID.randomUUID(), name, description, isControlTag);
+ }
+
+ public DefaultTagDefinition(final UUID id, final String name, final String description, final Boolean isControlTag) {
+ this(id, name, description, isControlTag, ImmutableList.<ObjectType>copyOf(ObjectType.values()));
+ }
+
+ public DefaultTagDefinition(final ControlTagType controlTag) {
+ this(controlTag.getId(), controlTag.toString(), controlTag.getDescription(), true, controlTag.getApplicableObjectTypes());
+ }
+
+ @JsonCreator
+ public DefaultTagDefinition(@JsonProperty("id") final UUID id,
+ @JsonProperty("name") final String name,
+ @JsonProperty("description") final String description,
+ @JsonProperty("controlTag") final Boolean controlTag,
+ @JsonProperty("applicableObjectTypes") final List<ObjectType> applicableObjectTypes) {
+ super(id);
+ this.name = name;
+ this.description = description;
+ this.controlTag = controlTag;
+ this.applicableObjectTypes = applicableObjectTypes;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public Boolean isControlTag() {
+ return controlTag;
+ }
+
+ @Override
+ public List<ObjectType> getApplicableObjectTypes() {
+ return applicableObjectTypes;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultTagDefinition");
+ sb.append("{name='").append(name).append('\'');
+ sb.append(", description='").append(description).append('\'');
+ sb.append(", controlTag=").append(controlTag);
+ sb.append(", applicableObjectTypes=").append(applicableObjectTypes);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultTagDefinition that = (DefaultTagDefinition) o;
+
+ if (applicableObjectTypes != null ? !applicableObjectTypes.equals(that.applicableObjectTypes) : that.applicableObjectTypes != null) {
+ return false;
+ }
+ if (controlTag != null ? !controlTag.equals(that.controlTag) : that.controlTag != null) {
+ return false;
+ }
+ if (description != null ? !description.equals(that.description) : that.description != null) {
+ return false;
+ }
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (controlTag != null ? controlTag.hashCode() : 0);
+ result = 31 * result + (applicableObjectTypes != null ? applicableObjectTypes.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/DefaultTagInternalApi.java b/util/src/main/java/org/killbill/billing/util/tag/DefaultTagInternalApi.java
new file mode 100644
index 0000000..e40f7f5
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/DefaultTagInternalApi.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.tag.dao.TagDao;
+import org.killbill.billing.util.tag.dao.TagDefinitionDao;
+import org.killbill.billing.util.tag.dao.TagDefinitionModelDao;
+import org.killbill.billing.util.tag.dao.TagModelDao;
+import org.killbill.billing.util.tag.dao.TagModelDaoHelper;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+public class DefaultTagInternalApi implements TagInternalApi {
+
+ private final TagDao tagDao;
+ private final TagDefinitionDao tagDefinitionDao;
+
+ @Inject
+ public DefaultTagInternalApi(final TagDao tagDao,
+ final TagDefinitionDao tagDefinitionDao) {
+ this.tagDao = tagDao;
+ this.tagDefinitionDao = tagDefinitionDao;
+ }
+
+ @Override
+ public List<TagDefinition> getTagDefinitions(final InternalTenantContext context) {
+ return ImmutableList.<TagDefinition>copyOf(Collections2.transform(tagDefinitionDao.getTagDefinitions(context),
+ new Function<TagDefinitionModelDao, TagDefinition>() {
+ @Override
+ public TagDefinition apply(final TagDefinitionModelDao input) {
+ return new DefaultTagDefinition(input, TagModelDaoHelper.isControlTag(input.getName()));
+ }
+ }));
+ }
+
+ @Override
+ public List<Tag> getTags(final UUID objectId, final ObjectType objectType, final InternalTenantContext context) {
+ return ImmutableList.<Tag>copyOf(Collections2.transform(tagDao.getTagsForObject(objectId, objectType, false, context),
+ new Function<TagModelDao, Tag>() {
+ @Override
+ public Tag apply(final TagModelDao input) {
+ return TagModelDaoHelper.isControlTag(input.getTagDefinitionId()) ?
+ new DefaultControlTag(ControlTagType.getTypeFromId(input.getTagDefinitionId()), objectType, objectId, input.getCreatedDate()) :
+ new DescriptiveTag(input.getTagDefinitionId(), objectType, objectId, input.getCreatedDate());
+ }
+ }));
+ }
+
+ @Override
+ public void addTag(final UUID objectId, final ObjectType objectType, final UUID tagDefinitionId, final InternalCallContext context)
+ throws TagApiException {
+ final TagModelDao tag = new TagModelDao(context.getCreatedDate(), tagDefinitionId, objectId, objectType);
+ tagDao.create(tag, context);
+
+ }
+
+ @Override
+ public void removeTag(final UUID objectId, final ObjectType objectType, final UUID tagDefinitionId, final InternalCallContext context)
+ throws TagApiException {
+ tagDao.deleteTag(objectId, objectType, tagDefinitionId, context);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/DescriptiveTag.java b/util/src/main/java/org/killbill/billing/util/tag/DescriptiveTag.java
new file mode 100644
index 0000000..7afb64b
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/DescriptiveTag.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.entity.EntityBase;
+
+public class DescriptiveTag extends EntityBase implements Tag {
+
+ private final UUID tagDefinitionId;
+ private final UUID objectId;
+ private final ObjectType objectType;
+
+ // use to hydrate objects from the persistence layer
+ public DescriptiveTag(final UUID id, final UUID tagDefinitionId, final ObjectType objectType, final UUID objectId, final DateTime createdDate) {
+ super(id, createdDate, createdDate);
+ this.tagDefinitionId = tagDefinitionId;
+ this.objectType = objectType;
+ this.objectId = objectId;
+ }
+
+ // use to create new objects
+ public DescriptiveTag(final UUID tagDefinitionId, final ObjectType objectType, final UUID objectId, final DateTime createdDate) {
+ super(UUID.randomUUID(), createdDate, createdDate);
+ this.tagDefinitionId = tagDefinitionId;
+ this.objectType = objectType;
+ this.objectId = objectId;
+ }
+
+ @Override
+ public UUID getTagDefinitionId() {
+ return tagDefinitionId;
+ }
+
+ @Override
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ @Override
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ @Override
+ public String toString() {
+ return "DescriptiveTag [tagDefinitionId=" + tagDefinitionId + ", id=" + id + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((tagDefinitionId == null) ? 0
+ : tagDefinitionId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final DescriptiveTag other = (DescriptiveTag) obj;
+ if (tagDefinitionId == null) {
+ if (other.tagDefinitionId != null) {
+ return false;
+ }
+ } else if (!tagDefinitionId.equals(other.tagDefinitionId)) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/template/translation/DefaultCatalogTranslator.java b/util/src/main/java/org/killbill/billing/util/template/translation/DefaultCatalogTranslator.java
new file mode 100644
index 0000000..0343da3
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/template/translation/DefaultCatalogTranslator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.template.translation;
+
+import com.google.inject.Inject;
+
+public class DefaultCatalogTranslator extends DefaultTranslatorBase {
+ @Inject
+ public DefaultCatalogTranslator(final TranslatorConfig config) {
+ super(config);
+ }
+
+ @Override
+ protected String getBundlePath() {
+ return config.getCatalogBundlePath();
+ }
+
+ @Override
+ protected String getTranslationType() {
+ return "catalog";
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/template/translation/DefaultTranslatorBase.java b/util/src/main/java/org/killbill/billing/util/template/translation/DefaultTranslatorBase.java
new file mode 100644
index 0000000..93dce57
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/template/translation/DefaultTranslatorBase.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.template.translation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.PropertyResourceBundle;
+import java.util.ResourceBundle;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.billing.util.LocaleUtils;
+import org.killbill.billing.util.config.catalog.UriAccessor;
+
+import com.google.inject.Inject;
+
+public abstract class DefaultTranslatorBase implements Translator {
+
+ protected final TranslatorConfig config;
+ protected final Logger log = LoggerFactory.getLogger(DefaultTranslatorBase.class);
+
+ @Inject
+ public DefaultTranslatorBase(final TranslatorConfig config) {
+ this.config = config;
+ }
+
+ protected abstract String getBundlePath();
+
+ /*
+ * string used for exception handling
+ */
+ protected abstract String getTranslationType();
+
+ @Override
+ public String getTranslation(final Locale locale, final String originalText) {
+ final String bundlePath = getBundlePath();
+ ResourceBundle bundle = getBundle(locale, bundlePath);
+
+ if ((bundle != null) && (bundle.containsKey(originalText))) {
+ return bundle.getString(originalText);
+ } else {
+ if (config.getDefaultLocale() == null) {
+ log.debug("No default locale configured, returning original text");
+ return originalText;
+ }
+
+ final Locale defaultLocale = LocaleUtils.toLocale(config.getDefaultLocale());
+ try {
+ bundle = getBundle(defaultLocale, bundlePath);
+
+ if ((bundle != null) && (bundle.containsKey(originalText))) {
+ return bundle.getString(originalText);
+ } else {
+ return originalText;
+ }
+ } catch (MissingResourceException mrex) {
+ log.warn("Missing translation bundle for locale {}", defaultLocale);
+ return originalText;
+ }
+ }
+ }
+
+ private ResourceBundle getBundle(final Locale locale, final String bundlePath) {
+ try {
+ // Try to load the bundle from the classpath first
+ return ResourceBundle.getBundle(bundlePath, locale);
+ } catch (MissingResourceException ignored) {
+ }
+
+ // Try to load it from a properties file
+ final String propertiesFileNameWithCountry = bundlePath + "_" + locale.getLanguage() + "_" + locale.getCountry() + ".properties";
+ ResourceBundle bundle = getBundleFromPropertiesFile(propertiesFileNameWithCountry);
+ if (bundle != null) {
+ return bundle;
+ } else {
+ final String propertiesFileName = bundlePath + "_" + locale.getLanguage() + ".properties";
+ bundle = getBundleFromPropertiesFile(propertiesFileName);
+ }
+
+ return bundle;
+ }
+
+ private ResourceBundle getBundleFromPropertiesFile(final String propertiesFileName) {
+ try {
+ final InputStream inputStream = UriAccessor.accessUri(propertiesFileName);
+ if (inputStream == null) {
+ return null;
+ } else {
+ return new PropertyResourceBundle(inputStream);
+ }
+ } catch (IllegalArgumentException iae) {
+ return null;
+ } catch (MissingResourceException mrex) {
+ return null;
+ } catch (URISyntaxException e) {
+ return null;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/timezone/DateAndTimeZoneContext.java b/util/src/main/java/org/killbill/billing/util/timezone/DateAndTimeZoneContext.java
new file mode 100644
index 0000000..872f0e5
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/timezone/DateAndTimeZoneContext.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.timezone;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Days;
+import org.joda.time.LocalDate;
+import org.joda.time.LocalTime;
+
+import org.killbill.clock.Clock;
+
+/**
+ * Used by entitlement and invoice to calculate:
+ * - a LocalDate from DateTime and the timeZone set on the account
+ * - A DateTime from a LocalDate and the referenceTime attached to the account.
+ */
+public final class DateAndTimeZoneContext {
+
+ private final LocalTime referenceTime;
+ private final int offsetFromUtc;
+ private final DateTimeZone accountTimeZone;
+ private final Clock clock;
+
+ public DateAndTimeZoneContext(final DateTime effectiveDateTime, final DateTimeZone accountTimeZone, final Clock clock) {
+ this.clock = clock;
+ this.referenceTime = effectiveDateTime != null ? effectiveDateTime.toLocalTime() : null;
+ this.accountTimeZone = accountTimeZone;
+ this.offsetFromUtc = computeOffsetFromUtc(effectiveDateTime, accountTimeZone);
+ }
+
+ static int computeOffsetFromUtc(final DateTime effectiveDateTime, final DateTimeZone accountTimeZone) {
+ final LocalDate localDateInAccountTimeZone = new LocalDate(effectiveDateTime, accountTimeZone);
+ final LocalDate localDateInUTC = new LocalDate(effectiveDateTime, DateTimeZone.UTC);
+ return Days.daysBetween(localDateInUTC, localDateInAccountTimeZone).getDays();
+ }
+
+ public LocalDate computeTargetDate(final DateTime targetDateTime) {
+ return new LocalDate(targetDateTime, accountTimeZone);
+ }
+
+
+ public DateTime computeUTCDateTimeFromLocalDate(final LocalDate invoiceItemEndDate) {
+ //
+ // Since we create the targetDate for next invoice using the date from the notificationQ, we need to make sure
+ // that this datetime once transformed into a LocalDate points to the correct day.
+ //
+ // All we need to do is figure is the transformation from DateTime (point in time) to LocalDate (date in account time zone)
+ // changed the day; if so, when we recompute a UTC date from LocalDate (date in account time zone), we can simply chose a reference
+ // time and apply the offset backward to end up on the right day
+ //
+ // We use clock.getUTCNow() to get the offset with account timezone but that may not be correct
+ // when we transition from standard time and daylight saving time. We could end up with a result
+ // that is slightly in advance and therefore results in a null invoice.
+ // We will fix that by re-inserting ourselves in the notificationQ if we detect that there is no invoice
+ // and yet the subscription is recurring and not cancelled.
+ //
+ return invoiceItemEndDate.toDateTime(referenceTime, DateTimeZone.UTC).plusDays(-offsetFromUtc);
+ }
+
+ public DateTime computeUTCDateTimeFromNow() {
+ final LocalDate now = computeTargetDate(clock.getUTCNow());
+ return computeUTCDateTimeFromLocalDate(now);
+ }
+
+}
diff --git a/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequest.java b/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequest.java
new file mode 100644
index 0000000..679e09d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequest.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.killbill.billing.util.userrequest;
+
+public interface CompletionUserRequest extends CompletionUserRequestNotifier, CompletionUserRequestWaiter {
+}
diff --git a/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestBase.java b/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestBase.java
new file mode 100644
index 0000000..3f99515
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestBase.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.userrequest;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeoutException;
+
+import org.killbill.billing.events.AccountChangeInternalEvent;
+import org.killbill.billing.events.AccountCreationInternalEvent;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.events.InvoiceCreationInternalEvent;
+import org.killbill.billing.events.NullInvoiceInternalEvent;
+import org.killbill.billing.events.PaymentErrorInternalEvent;
+import org.killbill.billing.events.PaymentInfoInternalEvent;
+import org.killbill.billing.events.PaymentPluginErrorInternalEvent;
+
+public class CompletionUserRequestBase implements CompletionUserRequest {
+
+ private static final long NANO_TO_MILLI_SEC = (1000L * 1000L);
+
+ private final List<BusInternalEvent> events;
+
+ private final UUID userToken;
+ private long timeoutMilliSec;
+
+ private boolean isCompleted;
+ private long initialTimeMilliSec;
+
+
+ public CompletionUserRequestBase(final UUID userToken) {
+ this.events = new LinkedList<BusInternalEvent>();
+ this.userToken = userToken;
+ this.isCompleted = false;
+ }
+
+ @Override
+ public List<BusInternalEvent> waitForCompletion(final long timeoutMilliSec) throws InterruptedException, TimeoutException {
+
+ this.timeoutMilliSec = timeoutMilliSec;
+ initialTimeMilliSec = currentTimeMillis();
+ synchronized (this) {
+ long remainingTimeMillisSec = getRemainingTimeMillis();
+ while (!isCompleted && remainingTimeMillisSec > 0) {
+ wait(remainingTimeMillisSec);
+ if (isCompleted) {
+ break;
+ }
+ remainingTimeMillisSec = getRemainingTimeMillis();
+ }
+ if (!isCompleted) {
+ throw new TimeoutException();
+ }
+ }
+ return events;
+ }
+
+ @Override
+ public void notifyForCompletion() {
+ synchronized (this) {
+ isCompleted = true;
+ notify();
+ }
+ }
+
+
+ private long currentTimeMillis() {
+ return System.nanoTime() / NANO_TO_MILLI_SEC;
+ }
+
+ private long getRemainingTimeMillis() {
+ return timeoutMilliSec - (currentTimeMillis() - initialTimeMilliSec);
+ }
+
+ @Override
+ public void onBusEvent(final BusInternalEvent curEvent) {
+
+ // Check if this is for us..
+ if (curEvent.getUserToken() == null ||
+ !curEvent.getUserToken().equals(userToken)) {
+ return;
+ }
+ events.add(curEvent);
+
+ switch (curEvent.getBusEventType()) {
+ case ACCOUNT_CREATE:
+ onAccountCreation((AccountCreationInternalEvent) curEvent);
+ break;
+ case ACCOUNT_CHANGE:
+ onAccountChange((AccountChangeInternalEvent) curEvent);
+ break;
+ case SUBSCRIPTION_TRANSITION:
+ // We only dispatch the event for the effective date and not the requested date since we have both
+ // for subscription events.
+ if (curEvent instanceof EffectiveSubscriptionInternalEvent) {
+ onSubscriptionBaseTransition((EffectiveSubscriptionInternalEvent) curEvent);
+ }
+ break;
+ case INVOICE_EMPTY:
+ onEmptyInvoice((NullInvoiceInternalEvent) curEvent);
+ break;
+ case INVOICE_CREATION:
+ onInvoiceCreation((InvoiceCreationInternalEvent) curEvent);
+ break;
+ case PAYMENT_INFO:
+ onPaymentInfo((PaymentInfoInternalEvent) curEvent);
+ break;
+ case PAYMENT_ERROR:
+ onPaymentError((PaymentErrorInternalEvent) curEvent);
+ break;
+ case PAYMENT_PLUGIN_ERROR:
+ onPaymentPluginError((PaymentPluginErrorInternalEvent) curEvent);
+ break;
+ default:
+ throw new RuntimeException("Unexpected event type " + curEvent.getBusEventType());
+ }
+ }
+
+
+ @Override
+ public void onAccountCreation(final AccountCreationInternalEvent curEvent) {
+ }
+
+ @Override
+ public void onAccountChange(final AccountChangeInternalEvent curEvent) {
+ }
+
+ @Override
+ public void onSubscriptionBaseTransition(final EffectiveSubscriptionInternalEvent curEventEffective) {
+ }
+
+ @Override
+ public void onInvoiceCreation(final InvoiceCreationInternalEvent curEvent) {
+ }
+
+ @Override
+ public void onEmptyInvoice(final NullInvoiceInternalEvent curEvent) {
+ }
+
+ @Override
+ public void onPaymentInfo(final PaymentInfoInternalEvent curEvent) {
+ }
+
+ @Override
+ public void onPaymentError(final PaymentErrorInternalEvent curEvent) {
+ }
+
+ @Override
+ public void onPaymentPluginError(final PaymentPluginErrorInternalEvent curEvent) {
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestNotifier.java b/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestNotifier.java
new file mode 100644
index 0000000..2bad350
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestNotifier.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.userrequest;
+
+
+import org.killbill.billing.events.BusInternalEvent;
+
+
+public interface CompletionUserRequestNotifier {
+
+ public void notifyForCompletion();
+
+ public void onBusEvent(final BusInternalEvent event);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestWaiter.java b/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestWaiter.java
new file mode 100644
index 0000000..6668dcf
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/userrequest/CompletionUserRequestWaiter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.userrequest;
+
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+import org.killbill.billing.events.AccountChangeInternalEvent;
+import org.killbill.billing.events.AccountCreationInternalEvent;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.events.InvoiceCreationInternalEvent;
+import org.killbill.billing.events.NullInvoiceInternalEvent;
+import org.killbill.billing.events.PaymentErrorInternalEvent;
+import org.killbill.billing.events.PaymentInfoInternalEvent;
+import org.killbill.billing.events.PaymentPluginErrorInternalEvent;
+
+public interface CompletionUserRequestWaiter {
+
+ public List<BusInternalEvent> waitForCompletion(final long timeoutMilliSec) throws InterruptedException, TimeoutException;
+
+ public void onAccountCreation(final AccountCreationInternalEvent curEvent);
+
+ public void onAccountChange(final AccountChangeInternalEvent curEvent);
+
+ public void onSubscriptionBaseTransition(final EffectiveSubscriptionInternalEvent curEventEffective);
+
+ public void onInvoiceCreation(final InvoiceCreationInternalEvent curEvent);
+
+ public void onEmptyInvoice(final NullInvoiceInternalEvent curEvent);
+
+ public void onPaymentInfo(final PaymentInfoInternalEvent curEvent);
+
+ public void onPaymentError(final PaymentErrorInternalEvent curEvent);
+
+ public void onPaymentPluginError(final PaymentPluginErrorInternalEvent curEvent);
+}
diff --git a/util/src/main/java/org/killbill/billing/util/validation/dao/DatabaseSchemaDao.java b/util/src/main/java/org/killbill/billing/util/validation/dao/DatabaseSchemaDao.java
new file mode 100644
index 0000000..002d35b
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/validation/dao/DatabaseSchemaDao.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.validation.dao;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+import javax.inject.Singleton;
+
+import org.skife.jdbi.v2.IDBI;
+
+import org.killbill.billing.util.validation.DefaultColumnInfo;
+
+import com.google.inject.Inject;
+
+@Singleton
+public class DatabaseSchemaDao {
+
+ private final DatabaseSchemaSqlDao dao;
+
+ @Inject
+ public DatabaseSchemaDao(final IDBI dbi) {
+ this.dao = dbi.onDemand(DatabaseSchemaSqlDao.class);
+ }
+
+ public List<DefaultColumnInfo> getColumnInfoList() {
+ return getColumnInfoList(null);
+ }
+
+ public List<DefaultColumnInfo> getColumnInfoList(@Nullable final String schemaName) {
+ return dao.getSchemaInfo(schemaName);
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.java b/util/src/main/java/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.java
new file mode 100644
index 0000000..6fe55eb
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.validation.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import org.killbill.billing.util.validation.DefaultColumnInfo;
+
+@UseStringTemplate3StatementLocator
+@RegisterMapper(DatabaseSchemaSqlDao.ColumnInfoMapper.class)
+public interface DatabaseSchemaSqlDao {
+
+ @SqlQuery
+ List<DefaultColumnInfo> getSchemaInfo(@Nullable @Bind("schemaName") final String schemaName);
+
+ class ColumnInfoMapper implements ResultSetMapper<DefaultColumnInfo> {
+
+ @Override
+ public DefaultColumnInfo map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+ final String tableName = r.getString("table_name");
+ final String columnName = r.getString("column_name");
+ final Integer scale = r.getInt("numeric_scale");
+ final Integer precision = r.getInt("numeric_precision");
+ final boolean isNullable = r.getBoolean("is_nullable");
+ final Integer maximumLength = r.getInt("character_maximum_length");
+ final String dataType = r.getString("data_type");
+
+ return new DefaultColumnInfo(tableName, columnName, scale, precision, isNullable, maximumLength, dataType);
+ }
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/validation/DefaultColumnInfo.java b/util/src/main/java/org/killbill/billing/util/validation/DefaultColumnInfo.java
new file mode 100644
index 0000000..243e89a
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/validation/DefaultColumnInfo.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.validation;
+
+import org.killbill.billing.util.api.ColumnInfo;
+
+public class DefaultColumnInfo implements ColumnInfo {
+
+ private final String tableName;
+ private final String columnName;
+ private final int scale;
+ private final int precision;
+ private final boolean isNullable;
+ private final int maximumLength;
+ private final String dataType;
+
+ public DefaultColumnInfo(final String tableName, final String columnName, final int scale, final int precision,
+ final boolean nullable, final int maximumLength, final String dataType) {
+ this.tableName = tableName;
+ this.columnName = columnName;
+ this.scale = scale;
+ this.precision = precision;
+ isNullable = nullable;
+ this.maximumLength = maximumLength;
+ this.dataType = dataType;
+ }
+
+ @Override
+ public String getTableName() {
+ return tableName;
+ }
+
+ @Override
+ public String getColumnName() {
+ return columnName;
+ }
+
+ public int getScale() {
+ return scale;
+ }
+
+ public int getPrecision() {
+ return precision;
+ }
+
+ public boolean getIsNullable() {
+ return isNullable;
+ }
+
+ public int getMaximumLength() {
+ return maximumLength;
+ }
+
+ @Override
+ public String getDataType() {
+ return dataType;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/validation/ValidationConfiguration.java b/util/src/main/java/org/killbill/billing/util/validation/ValidationConfiguration.java
new file mode 100644
index 0000000..f65afdc
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/validation/ValidationConfiguration.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.validation;
+
+import java.util.HashMap;
+
+public class ValidationConfiguration extends HashMap<String, DefaultColumnInfo> {
+ public void addMapping(final String propertyName, final DefaultColumnInfo columnInfo) {
+ super.put(propertyName, columnInfo);
+ }
+
+ public boolean hasMapping(final String propertyName) {
+ return super.get(propertyName) != null;
+ }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/validation/ValidationManager.java b/util/src/main/java/org/killbill/billing/util/validation/ValidationManager.java
new file mode 100644
index 0000000..253ed8f
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/validation/ValidationManager.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.validation;
+
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.sql.Types;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.killbill.billing.util.validation.dao.DatabaseSchemaDao;
+
+import com.google.inject.Inject;
+
+public class ValidationManager {
+
+ private final DatabaseSchemaDao dao;
+
+ // table name, string name, column info
+ private final Map<String, Map<String, DefaultColumnInfo>> columnInfoMap = new HashMap<String, Map<String, DefaultColumnInfo>>();
+ private final Map<Class, ValidationConfiguration> configurations = new HashMap<Class, ValidationConfiguration>();
+
+ @Inject
+ public ValidationManager(final DatabaseSchemaDao dao) {
+ this.dao = dao;
+ }
+
+ // replaces existing schema information with the information for the specified schema
+ public void loadSchemaInformation(final String schemaName) {
+ columnInfoMap.clear();
+
+ // get schema information and map it to columnInfo
+ final List<DefaultColumnInfo> columnInfoList = dao.getColumnInfoList(schemaName);
+ for (final DefaultColumnInfo columnInfo : columnInfoList) {
+ final String tableName = columnInfo.getTableName();
+
+ if (!columnInfoMap.containsKey(tableName)) {
+ columnInfoMap.put(tableName, new HashMap<String, DefaultColumnInfo>());
+ }
+
+ columnInfoMap.get(tableName).put(columnInfo.getColumnName(), columnInfo);
+ }
+ }
+
+ public Collection<DefaultColumnInfo> getTableInfo(final String tableName) {
+ return columnInfoMap.get(tableName).values();
+ }
+
+ public DefaultColumnInfo getColumnInfo(final String tableName, final String columnName) {
+ return (columnInfoMap.get(tableName) == null) ? null : columnInfoMap.get(tableName).get(columnName);
+ }
+
+ public boolean validate(final Object o) {
+ final ValidationConfiguration configuration = getConfiguration(o.getClass());
+
+ // if no configuration exists for this class, the object is valid
+ if (configuration == null) {
+ return true;
+ }
+
+ final Class clazz = o.getClass();
+ for (final String propertyName : configuration.keySet()) {
+ try {
+ final Field field = clazz.getDeclaredField(propertyName);
+ if (!field.isAccessible()) {
+ field.setAccessible(true);
+ }
+
+ final Object value = field.get(o);
+
+ final DefaultColumnInfo columnInfo = configuration.get(propertyName);
+ if (columnInfo == null) {
+ // no column info means the property hasn't been properly mapped; suppress validation
+ return true;
+ }
+
+ if (!hasValidNullability(columnInfo, value)) {
+ return false;
+ }
+ if (!isValidLengthString(columnInfo, value)) {
+ return false;
+ }
+ if (!isValidLengthChar(columnInfo, value)) {
+ return false;
+ }
+ if (!hasValidPrecision(columnInfo, value)) {
+ return false;
+ }
+ if (!hasValidScale(columnInfo, value)) {
+ return false;
+ }
+ } catch (NoSuchFieldException e) {
+ // if the field doesn't exist, assume the configuration is faulty and skip this property
+ } catch (IllegalAccessException e) {
+ // TODO: something? deliberate no op?
+ }
+
+ }
+
+ return true;
+ }
+
+ private boolean hasValidNullability(final DefaultColumnInfo columnInfo, final Object value) {
+ if (!columnInfo.getIsNullable()) {
+ if (value == null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean isValidLengthString(final DefaultColumnInfo columnInfo, final Object value) {
+ if (columnInfo.getMaximumLength() != 0) {
+ // H2 will report a character_maximum_length for decimal
+ if (value != null && value instanceof String) {
+ if (value.toString().length() > columnInfo.getMaximumLength()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private boolean isValidLengthChar(final DefaultColumnInfo columnInfo, final Object value) {
+ // MySQL reports data_type as Strings, H2 as SQLTypes
+ if (columnInfo.getDataType().equals("char") || columnInfo.getDataType().equals(String.valueOf(Types.CHAR))) {
+ if (value == null) {
+ return false;
+ } else {
+ if (value.toString().length() != columnInfo.getMaximumLength()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private boolean hasValidPrecision(final DefaultColumnInfo columnInfo, final Object value) {
+ if (columnInfo.getPrecision() != 0) {
+ // H2 will report a numeric precision for varchar columns
+ if (value != null && !(value instanceof String)) {
+ final BigDecimal bigDecimalValue = new BigDecimal(value.toString());
+ if (bigDecimalValue.precision() > columnInfo.getPrecision()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private boolean hasValidScale(final DefaultColumnInfo columnInfo, final Object value) {
+ if (columnInfo.getScale() != 0) {
+ if (value != null) {
+ final BigDecimal bigDecimalValue = new BigDecimal(value.toString());
+ if (bigDecimalValue.scale() > columnInfo.getScale()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public boolean hasConfiguration(final Class clazz) {
+ return configurations.containsKey(clazz);
+ }
+
+ public ValidationConfiguration getConfiguration(final Class clazz) {
+ return configurations.get(clazz);
+ }
+
+ public void setConfiguration(final Class clazz, final String propertyName, final DefaultColumnInfo columnInfo) {
+ if (!configurations.containsKey(clazz)) {
+ configurations.put(clazz, new ValidationConfiguration());
+ }
+
+ configurations.get(clazz).addMapping(propertyName, columnInfo);
+ }
+}
diff --git a/util/src/main/resources/accountRecordIdSanity.sql b/util/src/main/resources/accountRecordIdSanity.sql
index 42ee9cf..aefecf6 100644
--- a/util/src/main/resources/accountRecordIdSanity.sql
+++ b/util/src/main/resources/accountRecordIdSanity.sql
@@ -289,7 +289,7 @@ from (
, class_name
from bus_events
where 1 = 1
- and class_name in ('com.ning.billing.account.api.user.DefaultAccountChangeEvent', 'com.ning.billing.invoice.api.user.DefaultNullInvoiceEvent', 'com.ning.billing.payment.api.DefaultPaymentInfoEvent', 'com.ning.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent', 'com.ning.billing.payment.api.DefaultPaymentErrorEvent')
+ and class_name in ('org.killbill.billing.account.api.user.DefaultAccountChangeEvent', 'org.killbill.billing.invoice.api.user.DefaultNullInvoiceEvent', 'org.killbill.billing.payment.api.DefaultPaymentInfoEvent', 'org.killbill.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent', 'org.killbill.billing.payment.api.DefaultPaymentErrorEvent')
union all
select
substr(event_json,position('objectId' in event_json) + 11, 36) id
@@ -297,7 +297,7 @@ from (
, class_name
from bus_events
where 1 = 1
- and class_name in ('com.ning.billing.util.tag.api.user.DefaultUserTagCreationEvent', 'com.ning.billing.util.tag.api.user.DefaultControlTagCreationEvent', 'com.ning.billing.util.tag.api.user.DefaultControlTagDeletionEvent', 'com.ning.billing.util.tag.api.user.DefaultUserTagDeletionEvent')
+ and class_name in ('org.killbill.billing.util.tag.api.user.DefaultUserTagCreationEvent', 'org.killbill.billing.util.tag.api.user.DefaultControlTagCreationEvent', 'org.killbill.billing.util.tag.api.user.DefaultControlTagDeletionEvent', 'org.killbill.billing.util.tag.api.user.DefaultUserTagDeletionEvent')
) be
left outer join accounts a using (id)
where 1 = 1
@@ -317,7 +317,7 @@ from (
, class_name
from bus_events
where 1 = 1
- and class_name in ('com.ning.billing.entitlement.api.user.DefaultRequestedSubscriptionEvent', 'com.ning.billing.entitlement.api.user.DefaultEffectiveSubscriptionEvent')
+ and class_name in ('org.killbill.billing.entitlement.api.user.DefaultRequestedSubscriptionEvent', 'org.killbill.billing.entitlement.api.user.DefaultEffectiveSubscriptionEvent')
) be
left outer join subscriptions s using (id)
where 1 = 1
@@ -337,7 +337,7 @@ from (
, class_name
from bus_events
where 1 = 1
- and class_name in ('com.ning.billing.invoice.api.user.DefaultInvoiceCreationEvent')
+ and class_name in ('org.killbill.billing.invoice.api.user.DefaultInvoiceCreationEvent')
) be
left outer join invoices i using (id)
where 1 = 1
@@ -357,7 +357,7 @@ from (
, class_name
from bus_events
where 1 = 1
- and class_name in ('com.ning.billing.overdue.applicator.DefaultOverdueChangeEvent')
+ and class_name in ('org.killbill.billing.overdue.applicator.DefaultOverdueChangeEvent')
) be
left outer join bundles b using (id)
where 1 = 1
@@ -439,7 +439,7 @@ from (
, class_name
from notifications
where 1 = 1
- and class_name = 'com.ning.billing.invoice.notification.NextBillingDateNotificationKey'
+ and class_name = 'org.killbill.billing.invoice.notification.NextBillingDateNotificationKey'
) n
left outer join subscriptions s using (id)
where 1 = 1
@@ -459,7 +459,7 @@ from (
, class_name
from notifications
where 1 = 1
- and class_name in ('com.ning.billing.ovedue.notification.OverdueCheckNotificationKey', 'com.ning.billing.irs.callbacks.CallbackNotificationKey')
+ and class_name in ('org.killbill.billing.ovedue.notification.OverdueCheckNotificationKey', 'org.killbill.billing.irs.callbacks.CallbackNotificationKey')
) n
left outer join bundles b using (id)
where 1 = 1
@@ -479,7 +479,7 @@ from (
, class_name
from notifications
where 1 = 1
- and class_name = 'com.ning.billing.payment.retry.PaymentRetryNotificationKey'
+ and class_name = 'org.killbill.billing.payment.retry.PaymentRetryNotificationKey'
) n
left outer join payments p using (id)
where 1 = 1
@@ -499,7 +499,7 @@ from (
, class_name
from notifications
where 1 = 1
- and class_name = 'com.ning.billing.entitlement.engine.core.EntitlementNotificationKey'
+ and class_name = 'org.killbill.billing.entitlement.engine.core.EntitlementNotificationKey'
) n
left outer join subscription_events se using (id)
where 1 = 1
@@ -652,4 +652,4 @@ and (
t.account_record_id != a.record_id
or t.account_record_id is null
)
-;
\ No newline at end of file
+;
util/src/main/resources/ehcache.xml 10(+5 -5)
diff --git a/util/src/main/resources/ehcache.xml b/util/src/main/resources/ehcache.xml
index 07e2a73..fb82f50 100644
--- a/util/src/main/resources/ehcache.xml
+++ b/util/src/main/resources/ehcache.xml
@@ -39,7 +39,7 @@
statistics="true"
>
<cacheEventListenerFactory
- class="com.ning.billing.util.cache.ExpirationListenerFactory"
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
properties=""/>
</cache>
@@ -53,7 +53,7 @@
statistics="true"
>
<cacheEventListenerFactory
- class="com.ning.billing.util.cache.ExpirationListenerFactory"
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
properties=""/>
</cache>
@@ -67,7 +67,7 @@
statistics="true"
>
<cacheEventListenerFactory
- class="com.ning.billing.util.cache.ExpirationListenerFactory"
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
properties=""/>
</cache>
@@ -82,7 +82,7 @@
statistics="true"
>
<cacheEventListenerFactory
- class="com.ning.billing.util.cache.ExpirationListenerFactory"
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
properties=""/>
</cache>
@@ -97,7 +97,7 @@
statistics="true"
>
<cacheEventListenerFactory
- class="com.ning.billing.util.cache.ExpirationListenerFactory"
+ class="org.killbill.billing.util.cache.ExpirationListenerFactory"
properties=""/>
</cache>
</ehcache>
diff --git a/util/src/main/resources/org/killbill/billing/util/customfield/dao/CustomFieldSqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/customfield/dao/CustomFieldSqlDao.sql.stg
new file mode 100644
index 0000000..8bc1d99
--- /dev/null
+++ b/util/src/main/resources/org/killbill/billing/util/customfield/dao/CustomFieldSqlDao.sql.stg
@@ -0,0 +1,60 @@
+group CustomFieldSqlDao: EntitySqlDao;
+
+andCheckSoftDeletionWithComma(prefix) ::= "and <prefix>is_active"
+
+tableName() ::= "custom_fields"
+
+tableFields(prefix) ::= <<
+ <prefix>object_id
+, <prefix>object_type
+, <prefix>is_active
+, <prefix>field_name
+, <prefix>field_value
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>updated_by
+, <prefix>updated_date
+>>
+
+tableValues() ::= <<
+ :objectId
+, :objectType
+, :isActive
+, :fieldName
+, :fieldValue
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+historyTableName() ::= "custom_field_history"
+
+markTagAsDeleted() ::= <<
+update <tableName()> t
+set t.is_active = 0
+where <idField("t.")> = :id
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+
+getCustomFieldsForObject() ::= <<
+select
+<allTableFields()>
+from <tableName()>
+where
+object_id = :objectId
+and object_type = :objectType
+and is_active
+<AND_CHECK_TENANT()>
+<defaultOrderBy()>
+;
+>>
+
+searchQuery(prefix) ::= <<
+ <idField(prefix)> = :searchKey
+ or <prefix>object_type like :likeSearchKey
+ or <prefix>field_name like :likeSearchKey
+ or <prefix>field_value like :likeSearchKey
+>>
diff --git a/util/src/main/resources/org/killbill/billing/util/dao/NonEntitySqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/dao/NonEntitySqlDao.sql.stg
new file mode 100644
index 0000000..b7e4d4b
--- /dev/null
+++ b/util/src/main/resources/org/killbill/billing/util/dao/NonEntitySqlDao.sql.stg
@@ -0,0 +1,117 @@
+group NonEntitySqlDao;
+
+getRecordIdFromObject(tableName) ::= <<
+select
+ record_id
+from <tableName>
+where id = :id
+;
+>>
+
+getIdFromObject(tableName) ::= <<
+select
+ id
+from <tableName>
+where record_id = :recordId
+;
+>>
+
+getAccountRecordIdFromAccountHistory() ::= <<
+select
+ target_record_id
+from account_history
+where id = :id
+;
+>>
+
+getAccountRecordIdFromAccount() ::= <<
+select
+ record_id
+from accounts
+where id = :id
+;
+>>
+
+getAccountRecordIdFromObjectOtherThanAccount(tableName) ::= <<
+select
+ account_record_id
+from <tableName>
+where id = :id
+;
+>>
+
+getTenantRecordIdFromTenant() ::= <<
+select
+ record_id
+from tenants
+where id = :id
+;
+>>
+
+getTenantRecordIdFromObjectOtherThanTenant(tableName) ::= <<
+select
+ tenant_record_id
+from <tableName>
+where id = :id
+;
+>>
+
+
+getLastHistoryRecordId(tableName) ::= <<
+select
+ max(record_id)
+from <tableName>
+where target_record_id = :targetRecordId
+;
+>>
+
+getHistoryTargetRecordId(tableName) ::= <<
+select
+ target_record_id
+from <tableName>
+where record_id = :recordId
+;
+>>
+
+getHistoryRecordIdIdMappings(tableName, historyTableName) ::= <<
+select
+ ht.record_id
+, t.id
+from <tableName> t
+join <historyTableName> ht on ht.target_record_id = t.record_id
+where t.account_record_id = :accountRecordId
+and t.tenant_record_id = :tenantRecordId
+;
+>>
+
+getHistoryRecordIdIdMappingsForAccountsTable(tableName, historyTableName) ::= <<
+select
+ ht.record_id
+, t.id
+from <tableName> t
+join <historyTableName> ht on ht.target_record_id = t.record_id
+where t.record_id = :accountRecordId
+and t.tenant_record_id = :tenantRecordId
+;
+>>
+
+getHistoryRecordIdIdMappingsForTablesWithoutAccountRecordId(tableName, historyTableName) ::= <<
+select
+ ht.record_id
+, t.id
+from <tableName> t
+join <historyTableName> ht on ht.target_record_id = t.record_id
+where 1 = 1
+and t.tenant_record_id = :tenantRecordId
+;
+>>
+
+getRecordIdIdMappings(tableName) ::= <<
+select
+ t.record_id
+, t.id
+from <tableName> t
+where t.account_record_id = :accountRecordId
+and t.tenant_record_id = :tenantRecordId
+;
+>>
\ No newline at end of file
diff --git a/util/src/main/resources/org/killbill/billing/util/ddl.sql b/util/src/main/resources/org/killbill/billing/util/ddl.sql
new file mode 100644
index 0000000..2704dc9
--- /dev/null
+++ b/util/src/main/resources/org/killbill/billing/util/ddl.sql
@@ -0,0 +1,240 @@
+/*! SET storage_engine=INNODB */;
+
+DROP TABLE IF EXISTS custom_fields;
+CREATE TABLE custom_fields (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ object_id char(36) NOT NULL,
+ object_type varchar(30) NOT NULL,
+ is_active bool DEFAULT true,
+ field_name varchar(30) NOT NULL,
+ field_value varchar(255),
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) DEFAULT NULL,
+ updated_date datetime DEFAULT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX custom_fields_id ON custom_fields(id);
+CREATE INDEX custom_fields_object_id_object_type ON custom_fields(object_id, object_type);
+CREATE INDEX custom_fields_tenant_account_record_id ON custom_fields(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS custom_field_history;
+CREATE TABLE custom_field_history (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ target_record_id int(11) unsigned NOT NULL,
+ object_id char(36) NOT NULL,
+ object_type varchar(30) NOT NULL,
+ is_active bool DEFAULT true,
+ field_name varchar(30),
+ field_value varchar(255),
+ change_type char(6) NOT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX custom_field_history_target_record_id ON custom_field_history(target_record_id);
+CREATE INDEX custom_field_history_object_id_object_type ON custom_fields(object_id, object_type);
+CREATE INDEX custom_field_history_tenant_account_record_id ON custom_field_history(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS tag_definitions;
+CREATE TABLE tag_definitions (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ name varchar(20) NOT NULL,
+ description varchar(200) NOT NULL,
+ is_active bool DEFAULT true,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX tag_definitions_id ON tag_definitions(id);
+CREATE INDEX tag_definitions_tenant_record_id ON tag_definitions(tenant_record_id);
+
+DROP TABLE IF EXISTS tag_definition_history;
+CREATE TABLE tag_definition_history (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ target_record_id int(11) unsigned NOT NULL,
+ name varchar(30) NOT NULL,
+ description varchar(200),
+ is_active bool DEFAULT true,
+ change_type char(6) NOT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX tag_definition_history_id ON tag_definition_history(id);
+CREATE INDEX tag_definition_history_target_record_id ON tag_definition_history(target_record_id);
+CREATE INDEX tag_definition_history_name ON tag_definition_history(name);
+CREATE INDEX tag_definition_history_tenant_record_id ON tag_definition_history(tenant_record_id);
+
+DROP TABLE IF EXISTS tags;
+CREATE TABLE tags (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ tag_definition_id char(36) NOT NULL,
+ object_id char(36) NOT NULL,
+ object_type varchar(30) NOT NULL,
+ is_active bool DEFAULT true,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE UNIQUE INDEX tags_id ON tags(id);
+CREATE INDEX tags_by_object ON tags(object_id);
+CREATE INDEX tags_tenant_account_record_id ON tags(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS tag_history;
+CREATE TABLE tag_history (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ target_record_id int(11) unsigned NOT NULL,
+ object_id char(36) NOT NULL,
+ object_type varchar(30) NOT NULL,
+ tag_definition_id char(36) NOT NULL,
+ is_active bool DEFAULT true,
+ change_type char(6) NOT NULL,
+ created_by varchar(50) NOT NULL,
+ created_date datetime NOT NULL,
+ updated_by varchar(50) NOT NULL,
+ updated_date datetime NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX tag_history_target_record_id ON tag_history(target_record_id);
+CREATE INDEX tag_history_by_object ON tags(object_id);
+CREATE INDEX tag_history_tenant_account_record_id ON tag_history(tenant_record_id, account_record_id);
+
+DROP TABLE IF EXISTS audit_log;
+CREATE TABLE audit_log (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ target_record_id int(11) NOT NULL,
+ table_name varchar(50) NOT NULL,
+ change_type char(6) NOT NULL,
+ created_date datetime NOT NULL,
+ created_by varchar(50) NOT NULL,
+ reason_code varchar(255) DEFAULT NULL,
+ comments varchar(255) DEFAULT NULL,
+ user_token char(36),
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX audit_log_fetch_target_record_id ON audit_log(table_name, target_record_id);
+CREATE INDEX audit_log_user_name ON audit_log(created_by);
+CREATE INDEX audit_log_tenant_account_record_id ON audit_log(tenant_record_id, account_record_id);
+CREATE INDEX audit_log_via_history ON audit_log(target_record_id, table_name, tenant_record_id);
+
+
+
+DROP TABLE IF EXISTS notifications;
+CREATE TABLE notifications (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ class_name varchar(256) NOT NULL,
+ event_json varchar(2048) NOT NULL,
+ user_token char(36),
+ created_date datetime NOT NULL,
+ creating_owner char(50) NOT NULL,
+ processing_owner char(50) DEFAULT NULL,
+ processing_available_date datetime DEFAULT NULL,
+ processing_state varchar(14) DEFAULT 'AVAILABLE',
+ error_count int(11) unsigned DEFAULT 0,
+ search_key1 int(11) unsigned default null,
+ search_key2 int(11) unsigned default null,
+ queue_name char(64) NOT NULL,
+ effective_date datetime NOT NULL,
+ future_user_token char(36),
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX `idx_comp_where` ON notifications (`effective_date`, `processing_state`,`processing_owner`,`processing_available_date`);
+CREATE INDEX `idx_update` ON notifications (`processing_state`,`processing_owner`,`processing_available_date`);
+CREATE INDEX `idx_get_ready` ON notifications (`effective_date`,`created_date`);
+CREATE INDEX notifications_tenant_account_record_id ON notifications(search_key2, search_key1);
+
+DROP TABLE IF EXISTS notifications_history;
+CREATE TABLE notifications_history (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ class_name varchar(256) NOT NULL,
+ event_json varchar(2048) NOT NULL,
+ user_token char(36),
+ created_date datetime NOT NULL,
+ creating_owner char(50) NOT NULL,
+ processing_owner char(50) DEFAULT NULL,
+ processing_available_date datetime DEFAULT NULL,
+ processing_state varchar(14) DEFAULT 'AVAILABLE',
+ error_count int(11) unsigned DEFAULT 0,
+ search_key1 int(11) unsigned default null,
+ search_key2 int(11) unsigned default null,
+ queue_name char(64) NOT NULL,
+ effective_date datetime NOT NULL,
+ future_user_token char(36),
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+
+DROP TABLE IF EXISTS bus_events;
+CREATE TABLE bus_events (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ class_name varchar(128) NOT NULL,
+ event_json varchar(2048) NOT NULL,
+ user_token char(36),
+ created_date datetime NOT NULL,
+ creating_owner char(50) NOT NULL,
+ processing_owner char(50) DEFAULT NULL,
+ processing_available_date datetime DEFAULT NULL,
+ processing_state varchar(14) DEFAULT 'AVAILABLE',
+ error_count int(11) unsigned DEFAULT 0,
+ search_key1 int(11) unsigned default null,
+ search_key2 int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+CREATE INDEX `idx_bus_where` ON bus_events (`processing_state`,`processing_owner`,`processing_available_date`);
+CREATE INDEX bus_events_tenant_account_record_id ON bus_events(search_key2, search_key1);
+
+DROP TABLE IF EXISTS bus_events_history;
+CREATE TABLE bus_events_history (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ class_name varchar(128) NOT NULL,
+ event_json varchar(2048) NOT NULL,
+ user_token char(36),
+ created_date datetime NOT NULL,
+ creating_owner char(50) NOT NULL,
+ processing_owner char(50) DEFAULT NULL,
+ processing_available_date datetime DEFAULT NULL,
+ processing_state varchar(14) DEFAULT 'AVAILABLE',
+ error_count int(11) unsigned DEFAULT 0,
+ search_key1 int(11) unsigned default null,
+ search_key2 int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
+
+drop table if exists sessions;
+create table sessions (
+ record_id int(11) unsigned not null auto_increment
+, start_timestamp datetime not null
+, last_access_time datetime default null
+, timeout int(11)
+, host varchar(100) default null
+, session_data mediumblob default null
+, primary key(record_id)
+) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
diff --git a/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
new file mode 100644
index 0000000..85031cd
--- /dev/null
+++ b/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
@@ -0,0 +1,422 @@
+group EntitySqlDao;
+
+
+/****************** To override in each EntitySqlDao template file *****************************/
+
+tableName() ::= ""
+
+/** Leave out id, account_record_id and tenant_record_id **/
+tableFields(prefix) ::= ""
+
+tableValues() ::= ""
+
+historyTableName() ::= ""
+
+historyTableFields(prefix) ::= <<
+ <targetRecordIdField(prefix)>
+, <changeTypeField(prefix)>
+, <tableFields(prefix)>
+>>
+
+historyTableValues() ::= <<
+ <targetRecordIdValue()>
+, <changeTypeValue()>
+, <tableValues()>
+>>
+
+/** Used for entities that can be soft deleted to make we exclude those entries in base calls getById(), get() **/
+andCheckSoftDeletionWithComma(prefix) ::= ""
+
+
+/** Add extra fields for SELECT queries **/
+extraTableFieldsWithComma(prefix) ::= ""
+
+defaultOrderBy(prefix) ::= <<
+order by <recordIdField(prefix)> ASC
+>>
+
+/****************** To override in each EntitySqlDao template file <end> *****************************/
+
+
+idField(prefix) ::= <<
+<prefix>id
+>>
+
+idValue() ::= ":id"
+
+recordIdField(prefix) ::= <<
+<prefix>record_id
+>>
+
+recordIdValue() ::= ":recordId"
+
+changeTypeField(prefix) ::= <<
+<prefix>change_type
+>>
+
+changeTypeValue() ::= ":changeType"
+
+targetRecordIdField(prefix) ::= <<
+<prefix>target_record_id
+>>
+
+targetRecordIdValue() ::= ":targetRecordId"
+
+/** Override this if the Entity isn't tied to an account **/
+accountRecordIdField(prefix) ::= <<
+<prefix>account_record_id
+>>
+
+
+accountRecordIdFieldWithComma(prefix) ::= <<
+, <accountRecordIdField(prefix)>
+>>
+
+accountRecordIdValue() ::= ":accountRecordId"
+
+accountRecordIdValueWithComma() ::= <<
+, <accountRecordIdValue()>
+>>
+
+tenantRecordIdField(prefix) ::= <<
+<prefix>tenant_record_id
+>>
+
+tenantRecordIdFieldWithComma(prefix) ::= <<
+, <tenantRecordIdField(prefix)>
+>>
+
+
+tenantRecordIdValue() ::= ":tenantRecordId"
+
+tenantRecordIdValueWithComma() ::= <<
+, <tenantRecordIdValue()>
+>>
+
+
+allTableFields(prefix) ::= <<
+ <recordIdField(prefix)>
+, <idField(prefix)>
+, <tableFields(prefix)>
+<extraTableFieldsWithComma(prefix)>
+<accountRecordIdFieldWithComma(prefix)>
+<tenantRecordIdFieldWithComma(prefix)>
+>>
+
+
+
+allTableValues() ::= <<
+ <recordIdValue()>
+, <idValue()>
+, <tableValues()>
+<accountRecordIdValueWithComma()>
+<tenantRecordIdValueWithComma()>
+>>
+
+
+allHistoryTableFields(prefix) ::= <<
+ <recordIdField(prefix)>
+, <idField(prefix)>
+, <targetRecordIdField(prefix)>
+, <historyTableFields(prefix)>
+<accountRecordIdFieldWithComma(prefix)>
+<tenantRecordIdFieldWithComma(prefix)>
+>>
+
+allHistoryTableValues() ::= <<
+ <recordIdValue()>
+, <idValue()>
+, <targetRecordIdValue()>
+, <historyTableValues()>
+<accountRecordIdValueWithComma()>
+<tenantRecordIdValueWithComma()>
+>>
+
+
+/** Macros used for multi-tenancy (almost any query should use them!) */
+CHECK_TENANT(prefix) ::= "<prefix>tenant_record_id = :tenantRecordId"
+AND_CHECK_TENANT(prefix) ::= "and <CHECK_TENANT(prefix)>"
+
+getAll() ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+where <CHECK_TENANT("t.")>
+<andCheckSoftDeletionWithComma("t.")>
+<defaultOrderBy("t.")>
+;
+>>
+
+get(offset, rowCount, orderBy) ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+where <CHECK_TENANT("t.")>
+<andCheckSoftDeletionWithComma("t.")>
+order by t.<orderBy>
+limit :offset, :rowCount
+;
+>>
+
+getCount() ::= <<
+select
+count(1) as count
+from <tableName()> t
+where <CHECK_TENANT("t.")>
+<andCheckSoftDeletionWithComma("t.")>
+;
+>>
+
+getById(id) ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+where <idField("t.")> = :id
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+getByRecordId(recordId) ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+where <recordIdField("t.")> = :recordId
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+/** Note: account_record_id can be NULL **/
+getByAccountRecordId(accountRecordId) ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+where (<accountRecordIdField("t.")> = :accountRecordId or (<accountRecordIdField("t.")> is null and :accountRecordId is null))
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+<defaultOrderBy("t.")>
+;
+>>
+
+getByAccountRecordIdIncludedDeleted(accountRecordId) ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+where (<accountRecordIdField("t.")> = :accountRecordId or (<accountRecordIdField("t.")> is null and :accountRecordId is null))
+<AND_CHECK_TENANT("t.")>
+<defaultOrderBy("t.")>
+;
+>>
+
+getHistoryTargetRecordId(recordId) ::= <<
+select
+<targetRecordIdField("t.")>
+from <historyTableName()> t
+where <recordIdField("t.")> = :recordId
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+getRecordId(id) ::= <<
+select
+ <recordIdField("t.")>
+from <tableName()> t
+where <idField("t.")> = :id
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+getRecordIdForTable(tableName) ::= <<
+select
+ <recordIdField("t.")>
+from <tableName> t
+where <idField("t.")> = :id
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+getHistoryRecordId(targetRecordId) ::= <<
+select
+ max(<recordIdField("t.")>)
+from <historyTableName()> t
+where <targetRecordIdField("t.")> = :targetRecordId
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+getHistoryRecordIdsForTable(historyTableName) ::= <<
+select
+ <recordIdField("t.")>
+from <historyTableName> t
+where <targetRecordIdField("t.")> = :targetRecordId
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+searchQuery(prefix) ::= <<
+1 = 1
+>>
+
+search() ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+where (<searchQuery("t.")>)
+<AND_CHECK_TENANT("t.")>
+order by <recordIdField("t.")> ASC
+limit :offset, :rowCount
+;
+>>
+
+getSearchCount() ::= <<
+select
+ count(1) as count
+from <tableName()> t
+where (<searchQuery("t.")>)
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+create() ::= <<
+insert into <tableName()> (
+ <idField()>
+, <tableFields()>
+<accountRecordIdFieldWithComma()>
+<tenantRecordIdFieldWithComma()>
+)
+values (
+ <idValue()>
+, <tableValues()>
+<accountRecordIdValueWithComma()>
+<tenantRecordIdValueWithComma()>
+)
+;
+>>
+
+/** Audits, History **/
+auditTableName() ::= "audit_log"
+
+auditTableFields(prefix) ::= <<
+ <prefix>id
+, <prefix>table_name
+, <prefix>target_record_id
+, <prefix>change_type
+, <prefix>created_by
+, <prefix>reason_code
+, <prefix>comments
+, <prefix>user_token
+, <prefix>created_date
+<if(accountRecordIdField(prefix))>, <accountRecordIdField(prefix)><endif>
+<if(tenantRecordIdField(prefix))>, <tenantRecordIdField(prefix)><endif>
+>>
+
+auditTableValues() ::= <<
+ :id
+, :tableName
+, :targetRecordId
+, :changeType
+, :createdBy
+, :reasonCode
+, :comments
+, :userToken
+, :createdDate
+<if(accountRecordIdField(""))>, <accountRecordIdValue()><endif>
+<if(tenantRecordIdField(""))>, <tenantRecordIdValue()><endif>
+>>
+
+
+addHistoryFromTransaction() ::= <<
+insert into <historyTableName()> (
+ <idField()>
+, <historyTableFields()>
+<accountRecordIdFieldWithComma()>
+<tenantRecordIdFieldWithComma()>
+)
+values (
+ <idValue()>
+, <historyTableValues()>
+<accountRecordIdValueWithComma()>
+<tenantRecordIdValueWithComma()>
+)
+;
+>>
+
+
+insertAuditFromTransaction() ::= <<
+insert into <auditTableName()> (
+<auditTableFields()>
+)
+values (
+<auditTableValues()>
+)
+;
+>>
+
+getAuditLogsForAccountRecordId() ::= <<
+select
+ <auditTableFields("t.")>
+from <auditTableName()> t
+where <accountRecordIdField("t.")> = :accountRecordId
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+order by t.table_name, <recordIdField("t.")> ASC
+;
+>>
+
+getAuditLogsForTableNameAndAccountRecordId() ::= <<
+select
+ <auditTableFields("t.")>
+from <auditTableName()> t
+where <accountRecordIdField("t.")> = :accountRecordId
+and t.table_name = :tableName
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+<defaultOrderBy("t.")>
+;
+>>
+
+getAuditLogsForTargetRecordId() ::= <<
+select
+ <auditTableFields("t.")>
+from <auditTableName()> t
+where t.target_record_id = :targetRecordId
+and t.table_name = :tableName
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+<defaultOrderBy("t.")>
+;
+>>
+
+getAuditLogsViaHistoryForTargetRecordId(historyTableName) ::= <<
+select
+ <auditTableFields("t.")>
+from <auditTableName()> t
+join (
+ select
+ <recordIdField("h.")> record_id
+ from <historyTableName> h
+ where <targetRecordIdField("h.")> = :targetRecordId
+ <andCheckSoftDeletionWithComma("t.")>
+ <AND_CHECK_TENANT("h.")>
+) history_record_ids on t.target_record_id = history_record_ids.record_id
+where t.table_name = :tableName
+<AND_CHECK_TENANT("t.")>
+<defaultOrderBy("t.")>
+;
+>>
+
+test() ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+where <CHECK_TENANT("t.")>
+<andCheckSoftDeletionWithComma("t.")>
+limit 1
+;
+>>
diff --git a/util/src/main/resources/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.sql.stg
new file mode 100644
index 0000000..fe228d2
--- /dev/null
+++ b/util/src/main/resources/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.sql.stg
@@ -0,0 +1,51 @@
+group JDBCSessionSqlDao;
+
+read() ::= <<
+select
+ record_id
+, start_timestamp
+, last_access_time
+, timeout
+, host
+, session_data
+from sessions
+where record_id = :recordId
+;
+>>
+
+create() ::= <<
+insert into sessions (
+ start_timestamp
+, last_access_time
+, timeout
+, host
+, session_data
+) values (
+ :startTimestamp
+, :lastAccessTime
+, :timeout
+, :host
+, :sessionData
+);
+>>
+
+update() ::= <<
+update sessions set
+ start_timestamp = :startTimestamp
+, last_access_time = :lastAccessTime
+, timeout = :timeout
+, host = :host
+, session_data = :sessionData
+where record_id = :recordId
+;
+>>
+
+delete() ::= <<
+delete from sessions
+where record_id = :recordId
+;
+>>
+
+getLastInsertId() ::= <<
+select LAST_INSERT_ID();
+>>
diff --git a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagDefinitionSqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagDefinitionSqlDao.sql.stg
new file mode 100644
index 0000000..08e0800
--- /dev/null
+++ b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagDefinitionSqlDao.sql.stg
@@ -0,0 +1,70 @@
+group TagDefinitionDao: EntitySqlDao;
+
+tableName() ::= "tag_definitions"
+
+andCheckSoftDeletionWithComma(prefix) ::= "and <prefix>is_active"
+
+tableFields(prefix) ::= <<
+ <prefix>name
+, <prefix>description
+, <prefix>is_active
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>updated_by
+, <prefix>updated_date
+>>
+
+tableValues() ::= <<
+ :name
+, :description
+, :isActive
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+accountRecordIdFieldWithComma(prefix) ::= ""
+
+accountRecordIdValueWithComma() ::= ""
+
+historyTableName() ::= "tag_definition_history"
+
+markTagDefinitionAsDeleted() ::= <<
+update <tableName()> t
+set t.is_active = 0
+where <idField("t.")> = :id
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+tagDefinitionUsageCount() ::= <<
+select
+ count(<idField("t.")>)
+from tags t
+where t.is_active
+and t.tag_definition_id = :id
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+getByName() ::= <<
+select
+ <allTableFields("t.")>
+from <tableName()> t
+where t.name = :name
+and t.is_active
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+getByIds(tag_definition_ids) ::= <<
+select
+ <allTableFields("t.")>
+from <tableName()> t
+where t.is_active
+and <idField("t.")> in (<tag_definition_ids: {id | :id_<i0>}; separator="," >)
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
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
new file mode 100644
index 0000000..efdc434
--- /dev/null
+++ b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
@@ -0,0 +1,131 @@
+group TagDao: EntitySqlDao;
+
+tableName() ::= "tags"
+
+andCheckSoftDeletionWithComma(prefix) ::= "and <prefix>is_active"
+
+tableFields(prefix) ::= <<
+ <prefix>tag_definition_id
+, <prefix>object_id
+, <prefix>object_type
+, <prefix>is_active
+, <prefix>created_by
+, <prefix>created_date
+, <prefix>updated_by
+, <prefix>updated_date
+>>
+
+tableValues() ::= <<
+ :tagDefinitionId
+, :objectId
+, :objectType
+, :isActive
+, :createdBy
+, :createdDate
+, :updatedBy
+, :updatedDate
+>>
+
+historyTableName() ::= "tag_history"
+
+markTagAsDeleted() ::= <<
+update <tableName()> t
+set t.is_active = 0
+where <idField("t.")> = :id
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+getTagsForObject() ::= <<
+select
+ <allTableFields("t.")>
+from <tableName()> t
+where t.is_active
+and t.object_id = :objectId
+and t.object_type = :objectType
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+getTagsForObjectIncludedDeleted() ::= <<
+select
+ <allTableFields("t.")>
+from <tableName()> t
+where 1 = 1
+and t.object_id = :objectId
+and t.object_type = :objectType
+<AND_CHECK_TENANT("t.")>
+;
+>>
+
+userAndSystemTagDefinitions() ::= <<
+ select
+ id
+ , name
+ , description
+ from tag_definitions
+ union
+ select
+ \'00000000-0000-0000-0000-000000000001\' id
+ , \'AUTO_PAY_OFF\' name
+ , \'Suspends payments until removed.\' description
+ union
+ select
+ \'00000000-0000-0000-0000-000000000002\' id
+ , \'AUTO_INVOICING_OFF\' name
+ , \'Suspends invoicing until removed.\' description
+ union
+ select
+ \'00000000-0000-0000-0000-000000000003\' id
+ , \'OVERDUE_ENFORCEMENT_OFF\' name
+ , \'Suspends overdue enforcement behaviour until removed.\' description
+ union
+ select
+ \'00000000-0000-0000-0000-000000000004\' id
+ , \'WRITTEN_OFF\' name
+ , \'Indicates that an invoice is written off. No billing or payment effect.\' description
+ union
+ select
+ \'00000000-0000-0000-0000-000000000005\' id
+ , \'MANUAL_PAY\' name
+ , \'Indicates that Killbill doesn\\'\\'t process payments for that account (external payments only)\' description
+ union
+ select
+ \'00000000-0000-0000-0000-000000000006\' id
+ , \'TEST\' name
+ , \'Indicates that this is a test account\' description
+ union
+ select
+ \'00000000-0000-0000-0000-000000000007\' id
+ , \'PARTNER\' name
+ , \'Indicates that this is a partner account\' description
+>>
+
+searchQuery(tagAlias, tagDefinitionAlias) ::= <<
+ <idField(tagAlias)> = :searchKey
+ or <tagAlias>object_type like :likeSearchKey
+ or <tagDefinitionAlias>name like :likeSearchKey
+ or <tagDefinitionAlias>description like :likeSearchKey
+>>
+
+search() ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+join (<userAndSystemTagDefinitions()>) td on td.id = t.tag_definition_id
+where (<searchQuery(tagAlias="t.", tagDefinitionAlias="td.")>)
+<AND_CHECK_TENANT("t.")>
+order by <recordIdField("t.")> ASC
+limit :offset, :rowCount
+;
+>>
+
+getSearchCount() ::= <<
+select
+ count(1) as count
+from <tableName()> t
+join (<userAndSystemTagDefinitions()>) td on td.id = t.tag_definition_id
+where (<searchQuery(tagAlias="t.", tagDefinitionAlias="td.")>)
+<AND_CHECK_TENANT("t.")>
+;
+>>
diff --git a/util/src/main/resources/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.sql.stg
new file mode 100644
index 0000000..e14db44
--- /dev/null
+++ b/util/src/main/resources/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.sql.stg
@@ -0,0 +1,10 @@
+group DatabaseSchemaSqlDao;
+
+getSchemaInfo(schemaName) ::= <<
+ SELECT TABLE_NAME, COLUMN_NAME, IS_NULLABLE, DATA_TYPE,
+ CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE
+ FROM information_schema.columns
+ WHERE TABLE_SCHEMA = <if(schemaName)>schemaName<else>schema()<endif>
+ ORDER BY TABLE_NAME, ORDINAL_POSITION;
+>>
+
diff --git a/util/src/test/java/org/killbill/billing/api/TestApiListener.java b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
new file mode 100644
index 0000000..a1d7fbd
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.api;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Stack;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+
+import org.killbill.billing.events.BlockingTransitionInternalEvent;
+import org.killbill.billing.events.CustomFieldEvent;
+import org.killbill.billing.events.EffectiveEntitlementInternalEvent;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+import org.killbill.billing.events.InvoiceAdjustmentInternalEvent;
+import org.killbill.billing.events.InvoiceCreationInternalEvent;
+import org.killbill.billing.events.PaymentErrorInternalEvent;
+import org.killbill.billing.events.PaymentInfoInternalEvent;
+import org.killbill.billing.events.PaymentPluginErrorInternalEvent;
+import org.killbill.billing.events.RepairSubscriptionInternalEvent;
+import org.killbill.billing.events.TagDefinitionInternalEvent;
+import org.killbill.billing.events.TagInternalEvent;
+
+import com.google.common.base.Joiner;
+import com.google.common.eventbus.Subscribe;
+
+import static com.jayway.awaitility.Awaitility.await;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class TestApiListener {
+
+ private static final Logger log = LoggerFactory.getLogger(TestApiListener.class);
+
+ private static final Joiner SPACE_JOINER = Joiner.on(" ");
+
+ private static final long DELAY = 25000;
+
+ private final List<NextEvent> nextExpectedEvent;
+ private final IDBI idbi;
+
+ private boolean isListenerFailed = false;
+ private String listenerFailedMsg;
+
+ private volatile boolean completed;
+
+ @Inject
+ public TestApiListener(final IDBI idbi) {
+ nextExpectedEvent = new Stack<NextEvent>();
+ this.completed = false;
+ this.idbi = idbi;
+ }
+
+ public void assertListenerStatus() {
+ try {
+ assertTrue(isCompleted(DELAY));
+ } catch (final Exception e) {
+ fail("assertListenerStatus didn't complete", e);
+ }
+
+ if (isListenerFailed) {
+ log.error(listenerFailedMsg);
+ Assert.fail(listenerFailedMsg);
+ }
+ }
+
+ public enum NextEvent {
+ MIGRATE_ENTITLEMENT,
+ MIGRATE_BILLING,
+ CREATE,
+ TRANSFER,
+ RE_CREATE,
+ CHANGE,
+ CANCEL,
+ UNCANCEL,
+ PAUSE,
+ RESUME,
+ PHASE,
+ BLOCK,
+ INVOICE,
+ INVOICE_ADJUSTMENT,
+ PAYMENT,
+ PAYMENT_ERROR,
+ PAYMENT_PLUGIN_ERROR,
+ REPAIR_BUNDLE,
+ TAG,
+ TAG_DEFINITION,
+ CUSTOM_FIELD,
+ }
+
+ @Subscribe
+ public void handleRepairSubscriptionEvents(final RepairSubscriptionInternalEvent event) {
+ log.info(String.format("Got RepairSubscriptionEvent event %s", event.toString()));
+ assertEqualsNicely(NextEvent.REPAIR_BUNDLE);
+ notifyIfStackEmpty();
+ }
+
+ @Subscribe
+ public void handleEntitlementEvents(final EffectiveEntitlementInternalEvent eventEffective) {
+ log.info(String.format("Got entitlement event %s", eventEffective.toString()));
+ switch (eventEffective.getTransitionType()) {
+ case BLOCK_BUNDLE:
+ assertEqualsNicely(NextEvent.PAUSE);
+ notifyIfStackEmpty();
+ break;
+ case UNBLOCK_BUNDLE:
+ assertEqualsNicely(NextEvent.RESUME);
+ notifyIfStackEmpty();
+ break;
+ }
+ }
+
+ @Subscribe
+ public void handleEntitlementEvents(final BlockingTransitionInternalEvent event) {
+ log.info(String.format("Got entitlement event %s", event.toString()));
+ assertEqualsNicely(NextEvent.BLOCK);
+ notifyIfStackEmpty();
+ }
+
+ @Subscribe
+ public void handleSubscriptionEvents(final EffectiveSubscriptionInternalEvent eventEffective) {
+ log.info(String.format("Got subscription event %s", eventEffective.toString()));
+ switch (eventEffective.getTransitionType()) {
+ case TRANSFER:
+ assertEqualsNicely(NextEvent.TRANSFER);
+ notifyIfStackEmpty();
+ break;
+ case MIGRATE_ENTITLEMENT:
+ assertEqualsNicely(NextEvent.MIGRATE_ENTITLEMENT);
+ notifyIfStackEmpty();
+ break;
+ case MIGRATE_BILLING:
+ assertEqualsNicely(NextEvent.MIGRATE_BILLING);
+ notifyIfStackEmpty();
+ break;
+ case CREATE:
+ assertEqualsNicely(NextEvent.CREATE);
+ notifyIfStackEmpty();
+ break;
+ case RE_CREATE:
+ assertEqualsNicely(NextEvent.RE_CREATE);
+ notifyIfStackEmpty();
+ break;
+ case CANCEL:
+ assertEqualsNicely(NextEvent.CANCEL);
+ notifyIfStackEmpty();
+ break;
+ case CHANGE:
+ assertEqualsNicely(NextEvent.CHANGE);
+ notifyIfStackEmpty();
+ break;
+ case UNCANCEL:
+ assertEqualsNicely(NextEvent.UNCANCEL);
+ notifyIfStackEmpty();
+ break;
+ case PHASE:
+ assertEqualsNicely(NextEvent.PHASE);
+ notifyIfStackEmpty();
+ break;
+ default:
+ throw new RuntimeException("Unexpected event type " + eventEffective.getRequestedTransitionTime());
+ }
+ }
+
+ @Subscribe
+ public synchronized void processTagEvent(final TagInternalEvent event) {
+ log.info(String.format("Got TagInternalEvent event %s", event.toString()));
+ assertEqualsNicely(NextEvent.TAG);
+ notifyIfStackEmpty();
+ }
+
+ @Subscribe
+ public synchronized void processCustomFieldEvent(final CustomFieldEvent event) {
+ log.info(String.format("Got CustomFieldEvent event %s", event.toString()));
+ assertEqualsNicely(NextEvent.CUSTOM_FIELD);
+ notifyIfStackEmpty();
+ }
+
+ @Subscribe
+ public synchronized void processTagDefinitonEvent(final TagDefinitionInternalEvent event) {
+ log.info(String.format("Got TagDefinitionInternalEvent event %s", event.toString()));
+ assertEqualsNicely(NextEvent.TAG_DEFINITION);
+ notifyIfStackEmpty();
+ }
+
+ @Subscribe
+ public void handleInvoiceEvents(final InvoiceCreationInternalEvent event) {
+ log.info(String.format("Got Invoice event %s", event.toString()));
+ assertEqualsNicely(NextEvent.INVOICE);
+ notifyIfStackEmpty();
+ }
+
+ @Subscribe
+ public void handleInvoiceAdjustmentEvents(final InvoiceAdjustmentInternalEvent event) {
+ log.info(String.format("Got Invoice adjustment event %s", event.toString()));
+ assertEqualsNicely(NextEvent.INVOICE_ADJUSTMENT);
+ notifyIfStackEmpty();
+ }
+
+ @Subscribe
+ public void handlePaymentEvents(final PaymentInfoInternalEvent event) {
+ log.info(String.format("Got PaymentInfo event %s", event.toString()));
+ assertEqualsNicely(NextEvent.PAYMENT);
+ notifyIfStackEmpty();
+ }
+
+ @Subscribe
+ public void handlePaymentErrorEvents(final PaymentErrorInternalEvent event) {
+ log.info(String.format("Got PaymentError event %s", event.toString()));
+ assertEqualsNicely(NextEvent.PAYMENT_ERROR);
+ notifyIfStackEmpty();
+ }
+
+ @Subscribe
+ public void handlePaymentPluginErrorEvents(final PaymentPluginErrorInternalEvent event) {
+ log.info(String.format("Got PaymentPluginError event %s", event.toString()));
+ assertEqualsNicely(NextEvent.PAYMENT_PLUGIN_ERROR);
+ notifyIfStackEmpty();
+ }
+
+ public void reset() {
+ synchronized (this) {
+ nextExpectedEvent.clear();
+ completed = true;
+
+ isListenerFailed = false;
+ listenerFailedMsg = null;
+ }
+ }
+
+ public void pushExpectedEvents(final NextEvent... events) {
+ for (final NextEvent event : events) {
+ pushExpectedEvent(event);
+ }
+ }
+
+ public void pushExpectedEvent(final NextEvent next) {
+ synchronized (this) {
+ nextExpectedEvent.add(next);
+ log.debug("Stacking expected event {}, got [{}]", next, SPACE_JOINER.join(nextExpectedEvent));
+ completed = false;
+ }
+ }
+
+ public boolean isCompleted(final long timeout) {
+ synchronized (this) {
+ if (completed) {
+ return completed;
+ }
+ long waitTimeMs = timeout;
+ do {
+ try {
+ final DateTime before = new DateTime();
+ wait(500);
+ if (completed) {
+ // TODO PIERRE Kludge alert!
+ // When we arrive here, we got notified by the current thread (Bus listener) that we received
+ // all expected events. But other handlers might still be processing them.
+ // Since there is only one bus thread, and that the test thread waits for all events to be processed,
+ // we're guaranteed that all are processed when the bus events table is empty.
+ await().atMost(timeout, TimeUnit.MILLISECONDS).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ final long inProcessingBusEvents = idbi.withHandle(new HandleCallback<Long>() {
+ @Override
+ public Long withHandle(final Handle handle) throws Exception {
+ return (Long) handle.select("select count(distinct record_id) count from bus_events").get(0).get("count");
+ }
+ });
+ log.debug("Events still in processing: " + inProcessingBusEvents);
+ return inProcessingBusEvents == 0;
+ }
+ });
+ return completed;
+ }
+ final DateTime after = new DateTime();
+ waitTimeMs -= after.getMillis() - before.getMillis();
+ } catch (final Exception ignore) {
+ log.error("isCompleted got interrupted ", ignore);
+ return false;
+ }
+ } while (waitTimeMs > 0 && !completed);
+ }
+
+ if (!completed) {
+ final Joiner joiner = Joiner.on(" ");
+ log.error("TestApiListener did not complete in " + timeout + " ms, remaining events are " + joiner.join(nextExpectedEvent));
+ }
+
+ return completed;
+ }
+
+ private void notifyIfStackEmpty() {
+ log.debug("TestApiListener notifyIfStackEmpty ENTER");
+ synchronized (this) {
+ if (nextExpectedEvent.isEmpty()) {
+ log.debug("notifyIfStackEmpty EMPTY");
+ completed = true;
+ notify();
+ }
+ }
+ log.debug("TestApiListener notifyIfStackEmpty EXIT");
+ }
+
+ private void assertEqualsNicely(final NextEvent received) {
+
+ synchronized (this) {
+ boolean foundIt = false;
+ final Iterator<NextEvent> it = nextExpectedEvent.iterator();
+ while (it.hasNext()) {
+ final NextEvent ev = it.next();
+ if (ev == received) {
+ it.remove();
+ foundIt = true;
+ log.debug("Found expected event {}. Yeah!", received);
+ break;
+ }
+ }
+ if (!foundIt) {
+ log.error("Received unexpected event " + received + "; remaining expected events [" + SPACE_JOINER.join(nextExpectedEvent) + "]");
+ failed("TestApiListener [ApiListenerStatus]: Received unexpected event " + received + "; remaining expected events [" + SPACE_JOINER.join(nextExpectedEvent) + "]");
+ }
+ }
+ }
+
+ private void failed(final String msg) {
+ this.isListenerFailed = true;
+ this.listenerFailedMsg = msg;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java b/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java
new file mode 100644
index 0000000..da53649
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.dao;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.dao.NonEntitySqlDao;
+import org.killbill.billing.util.dao.TableName;
+
+public class MockNonEntityDao implements NonEntityDao {
+
+ @Override
+ public Long retrieveRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+ return null;
+ }
+
+ @Override
+ public Long retrieveAccountRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+ return null;
+ }
+
+ @Override
+ public Long retrieveTenantRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+ return null;
+ }
+
+ @Override
+ public Long retrieveLastHistoryRecordIdFromTransaction(final Long targetRecordId, final TableName tableName, final NonEntitySqlDao transactional) {
+ return null;
+ }
+
+ @Override
+ public Long retrieveHistoryTargetRecordId(final Long recordId, final TableName tableName) {
+ return null;
+ }
+
+ @Override
+ public UUID retrieveIdFromObject(final Long recordId, final ObjectType objectType) {
+ return null;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/dbi/DBIProvider.java b/util/src/test/java/org/killbill/billing/dbi/DBIProvider.java
new file mode 100644
index 0000000..feb9592
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/dbi/DBIProvider.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.dbi;
+
+import javax.sql.DataSource;
+
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.tweak.transactions.SerializableTransactionRunner;
+
+import org.killbill.billing.util.dao.AuditLogModelDaoMapper;
+import org.killbill.billing.util.dao.DateTimeArgumentFactory;
+import org.killbill.billing.util.dao.DateTimeZoneArgumentFactory;
+import org.killbill.billing.util.dao.EnumArgumentFactory;
+import org.killbill.billing.util.dao.LocalDateArgumentFactory;
+import org.killbill.billing.util.dao.RecordIdIdMappingsMapper;
+import org.killbill.billing.util.dao.UUIDArgumentFactory;
+import org.killbill.billing.util.dao.UuidMapper;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class DBIProvider implements Provider<IDBI> {
+
+ private final DataSource ds;
+
+ @Inject
+ public DBIProvider(final DataSource ds) {
+ this.ds = ds;
+ }
+
+ @Override
+ public IDBI get() {
+ final DBI dbi = new DBI(ds);
+ dbi.registerArgumentFactory(new UUIDArgumentFactory());
+ dbi.registerArgumentFactory(new DateTimeZoneArgumentFactory());
+ dbi.registerArgumentFactory(new DateTimeArgumentFactory());
+ dbi.registerArgumentFactory(new LocalDateArgumentFactory());
+ dbi.registerArgumentFactory(new EnumArgumentFactory());
+ dbi.registerMapper(new UuidMapper());
+ dbi.registerMapper(new AuditLogModelDaoMapper());
+ dbi.registerMapper(new RecordIdIdMappingsMapper());
+
+ // Restart transactions in case of deadlocks
+ dbi.setTransactionHandler(new SerializableTransactionRunner());
+ //final SQLLog log = new Log4JLog();
+ //dbi.setSQLLog(log);
+
+ return dbi;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/dbi/LoggingOutputStream.java b/util/src/test/java/org/killbill/billing/dbi/LoggingOutputStream.java
new file mode 100644
index 0000000..4bda2b4
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/dbi/LoggingOutputStream.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.dbi;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.slf4j.Logger;
+
+/**
+ * Adapted from http://www.mail-archive.com/user@slf4j.org/msg00674.html for slf4j.
+ * <p/>
+ * An OutputStream that flushes out to a Category.<p>
+ * <p/>
+ * Note that no data is written out to the Category until the stream is
+ * flushed or closed.<p>
+ * <p/>
+ * Example:<pre>
+ * // make sure everything sent to System.err is logged
+ * System.setErr(new PrintStream(new
+ * LoggingOutputStream(Logger.getRootCategory(),
+ * Level.WARN), true));
+ * <p/>
+ * // make sure everything sent to System.out is also logged
+ * System.setOut(new PrintStream(new
+ * LoggingOutputStream(Logger.getRootCategory(),
+ * Level.INFO), true));
+ * </pre>
+ *
+ * @author <a href="[EMAIL PROTECTED]">Jim Moore</a>
+ * @see {{http://www.mail-archive.com/user@slf4j.org/msg00674.html}}
+ */
+
+public class LoggingOutputStream extends OutputStream {
+ /**
+ * Used to maintain the contract of [EMAIL PROTECTED] #close()}.
+ */
+ private boolean hasBeenClosed = false;
+
+ /**
+ * The internal buffer where data is stored.
+ */
+ private byte[] buf;
+
+ /**
+ * The number of valid bytes in the buffer. This value is always
+ * in the range <tt>0</tt> through <tt>buf.length</tt>; elements
+ * <tt>buf[0]</tt> through <tt>buf[count-1]</tt> contain valid
+ * byte data.
+ */
+ private int count;
+
+ /**
+ * Remembers the size of the buffer for speed.
+ */
+ private int bufLength;
+
+ /**
+ * The default number of bytes in the buffer. =2048
+ */
+ public static final int DEFAULT_BUFFER_LENGTH = 2048;
+
+ /**
+ * The category to write to.
+ */
+ private final Logger logger;
+
+ /**
+ * Creates the LoggingOutputStream to flush to the given Category.
+ *
+ * @param log the Logger to write to
+ * @throws IllegalArgumentException if log == null
+ */
+ public LoggingOutputStream(final Logger log) throws IllegalArgumentException {
+ if (log == null) {
+ throw new IllegalArgumentException("log == null");
+ }
+
+ logger = log;
+ bufLength = DEFAULT_BUFFER_LENGTH;
+ buf = new byte[DEFAULT_BUFFER_LENGTH];
+ count = 0;
+ }
+
+ /**
+ * Closes this output stream and releases any system resources
+ * associated with this stream. The general contract of
+ * <code>close</code>
+ * is that it closes the output stream. A closed stream cannot
+ * perform
+ * output operations and cannot be reopened.
+ */
+ public void close() {
+ flush();
+ hasBeenClosed = true;
+ }
+
+ /**
+ * Writes the specified byte to this output stream. The general
+ * contract for <code>write</code> is that one byte is written
+ * to the output stream. The byte to be written is the eight
+ * low-order bits of the argument <code>b</code>. The 24
+ * high-order bits of <code>b</code> are ignored.
+ *
+ * @param b the <code>byte</code> to write
+ * @throws java.io.IOException if an I/O error occurs. In particular, an
+ * <code>IOException</code> may be
+ * thrown if the output stream has been closed.
+ */
+ public void write(final int b) throws IOException {
+ if (hasBeenClosed) {
+ throw new IOException("The stream has been closed.");
+ }
+
+ if (((char) b) == '\r' || ((char) b) == '\n') {
+ return;
+ }
+
+ // would this be writing past the buffer?
+
+ if (count == bufLength) {
+ // grow the buffer
+ final int newBufLength = bufLength + DEFAULT_BUFFER_LENGTH;
+ final byte[] newBuf = new byte[newBufLength];
+
+ System.arraycopy(buf, 0, newBuf, 0, bufLength);
+ buf = newBuf;
+
+ bufLength = newBufLength;
+ }
+
+ buf[count] = (byte) b;
+
+ count++;
+ }
+
+ /**
+ * Flushes this output stream and forces any buffered output bytes
+ * to be written out. The general contract of <code>flush</code> is
+ * that calling it is an indication that, if any bytes previously
+ * written have been buffered by the implementation of the output
+ * stream, such bytes should immediately be written to their
+ * intended destination.
+ */
+ public void flush() {
+ if (count == 0) {
+ return;
+ }
+
+ // don't print out blank lines; flushing from PrintStream puts
+
+ // For linux system
+
+ if (count == 1 && ((char) buf[0]) == '\n') {
+ reset();
+ return;
+ }
+
+ // For mac system
+
+ if (count == 1 && ((char) buf[0]) == '\r') {
+ reset();
+ return;
+ }
+
+ // On windows system
+
+ if (count == 2 && (char) buf[0] == '\r' && (char) buf[1] == '\n') {
+ reset();
+ return;
+ }
+
+ final byte[] theBytes = new byte[count];
+ System.arraycopy(buf, 0, theBytes, 0, count);
+ logger.debug(new String(theBytes));
+ reset();
+ }
+
+ private void reset() {
+ // not resetting the buffer -- assuming that if it grew then it will likely grow similarly again
+ count = 0;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/DBTestingHelper.java b/util/src/test/java/org/killbill/billing/DBTestingHelper.java
new file mode 100644
index 0000000..3221a72
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/DBTestingHelper.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing;
+
+import java.io.IOException;
+
+import org.skife.jdbi.v2.IDBI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.killbill.commons.embeddeddb.EmbeddedDB;
+import org.killbill.commons.embeddeddb.h2.H2EmbeddedDB;
+import org.killbill.commons.embeddeddb.mysql.MySQLEmbeddedDB;
+import org.killbill.commons.embeddeddb.mysql.MySQLStandaloneDB;
+import org.killbill.billing.dbi.DBIProvider;
+import org.killbill.billing.util.io.IOUtils;
+
+import com.google.common.io.Resources;
+
+public class DBTestingHelper {
+
+ private static final Logger log = LoggerFactory.getLogger(DBTestingHelper.class);
+
+ protected static EmbeddedDB instance;
+
+ public static synchronized EmbeddedDB get() {
+ if (instance == null) {
+ if ("true".equals(System.getProperty("org.killbill.billing.dbi.test.h2"))) {
+ log.info("Using h2 as the embedded database");
+ instance = new H2EmbeddedDB();
+ } else {
+ if (isUsingLocalInstance()) {
+ log.info("Using MySQL local database");
+ final String databaseName = System.getProperty("org.killbill.billing.dbi.test.localDb.database", "killbill");
+ final String username = System.getProperty("org.killbill.billing.dbi.test.localDb.password", "root");
+ final String password = System.getProperty("org.killbill.billing.dbi.test.localDb.username", "root");
+ instance = new MySQLStandaloneDB(databaseName, username, password);
+ } else {
+ log.info("Using MySQL as the embedded database");
+ instance = new MySQLEmbeddedDB();
+ }
+ }
+ }
+ return instance;
+ }
+
+ public static synchronized IDBI getDBI() throws IOException {
+ return new DBIProvider(get().getDataSource()).get();
+ }
+
+ public static synchronized void start() throws IOException {
+ final EmbeddedDB instance = get();
+ instance.initialize();
+ instance.start();
+
+ if (isUsingLocalInstance()) {
+ return;
+ }
+
+ // We always want the accounts and tenants table
+ instance.executeScript("drop table if exists accounts;" +
+ "CREATE TABLE accounts (\n" +
+ " record_id int(11) unsigned NOT NULL AUTO_INCREMENT,\n" +
+ " id char(36) NOT NULL,\n" +
+ " external_key varchar(128) NULL,\n" +
+ " email varchar(128) NOT NULL,\n" +
+ " name varchar(100) NOT NULL,\n" +
+ " first_name_length int NOT NULL,\n" +
+ " currency char(3) DEFAULT NULL,\n" +
+ " billing_cycle_day_local int DEFAULT NULL,\n" +
+ " billing_cycle_day_utc int DEFAULT NULL,\n" +
+ " payment_method_id char(36) DEFAULT NULL,\n" +
+ " time_zone varchar(50) DEFAULT NULL,\n" +
+ " locale varchar(5) DEFAULT NULL,\n" +
+ " address1 varchar(100) DEFAULT NULL,\n" +
+ " address2 varchar(100) DEFAULT NULL,\n" +
+ " company_name varchar(50) DEFAULT NULL,\n" +
+ " city varchar(50) DEFAULT NULL,\n" +
+ " state_or_province varchar(50) DEFAULT NULL,\n" +
+ " country varchar(50) DEFAULT NULL,\n" +
+ " postal_code varchar(16) DEFAULT NULL,\n" +
+ " phone varchar(25) DEFAULT NULL,\n" +
+ " migrated bool DEFAULT false,\n" +
+ " is_notified_for_invoices boolean NOT NULL,\n" +
+ " created_date datetime NOT NULL,\n" +
+ " created_by varchar(50) NOT NULL,\n" +
+ " updated_date datetime DEFAULT NULL,\n" +
+ " updated_by varchar(50) DEFAULT NULL,\n" +
+ " tenant_record_id int(11) unsigned default null,\n" +
+ " PRIMARY KEY(record_id)\n" +
+ ");");
+ instance.executeScript("DROP TABLE IF EXISTS tenants;\n" +
+ "CREATE TABLE tenants (\n" +
+ " record_id int(11) unsigned NOT NULL AUTO_INCREMENT,\n" +
+ " id char(36) NOT NULL,\n" +
+ " external_key varchar(128) NULL,\n" +
+ " api_key varchar(128) NULL,\n" +
+ " api_secret varchar(128) NULL,\n" +
+ " api_salt varchar(128) NULL,\n" +
+ " created_date datetime NOT NULL,\n" +
+ " created_by varchar(50) NOT NULL,\n" +
+ " updated_date datetime DEFAULT NULL,\n" +
+ " updated_by varchar(50) DEFAULT NULL,\n" +
+ " PRIMARY KEY(record_id)\n" +
+ ");");
+
+ // We always want the basic tables when we do account_record_id lookups (e.g. for custom fields, tags or junction)
+ instance.executeScript("DROP TABLE IF EXISTS bundles;\n" +
+ "CREATE TABLE bundles (\n" +
+ " record_id int(11) unsigned NOT NULL AUTO_INCREMENT,\n" +
+ " id char(36) NOT NULL,\n" +
+ " external_key varchar(64) NOT NULL,\n" +
+ " account_id char(36) NOT NULL,\n" +
+ " last_sys_update_date datetime,\n" +
+ " created_by varchar(50) NOT NULL,\n" +
+ " created_date datetime NOT NULL,\n" +
+ " updated_by varchar(50) NOT NULL,\n" +
+ " updated_date datetime NOT NULL,\n" +
+ " account_record_id int(11) unsigned default null,\n" +
+ " tenant_record_id int(11) unsigned default null,\n" +
+ " PRIMARY KEY(record_id)\n" +
+ ");");
+ instance.executeScript("DROP TABLE IF EXISTS subscriptions;\n" +
+ "CREATE TABLE subscriptions (\n" +
+ " record_id int(11) unsigned NOT NULL AUTO_INCREMENT,\n" +
+ " id char(36) NOT NULL,\n" +
+ " bundle_id char(36) NOT NULL,\n" +
+ " category varchar(32) NOT NULL,\n" +
+ " start_date datetime NOT NULL,\n" +
+ " bundle_start_date datetime NOT NULL,\n" +
+ " active_version int(11) DEFAULT 1,\n" +
+ " charged_through_date datetime DEFAULT NULL,\n" +
+ " paid_through_date datetime DEFAULT NULL,\n" +
+ " created_by varchar(50) NOT NULL,\n" +
+ " created_date datetime NOT NULL,\n" +
+ " updated_by varchar(50) NOT NULL,\n" +
+ " updated_date datetime NOT NULL,\n" +
+ " account_record_id int(11) unsigned default null,\n" +
+ " tenant_record_id int(11) unsigned default null,\n" +
+ " PRIMARY KEY(record_id)\n" +
+ ");");
+
+ // HACK (PIERRE): required by invoice tests which perform payments lookups to find the account record id for the internal callcontext
+ instance.executeScript("DROP TABLE IF EXISTS payments;\n" +
+ "CREATE TABLE payments (\n" +
+ " record_id int(11) unsigned NOT NULL AUTO_INCREMENT,\n" +
+ " id char(36) NOT NULL,\n" +
+ " account_id char(36) NOT NULL,\n" +
+ " invoice_id char(36) NOT NULL,\n" +
+ " payment_method_id char(36) NOT NULL,\n" +
+ " amount numeric(15,9),\n" +
+ " currency char(3),\n" +
+ " effective_date datetime,\n" +
+ " payment_status varchar(50),\n" +
+ " created_by varchar(50) NOT NULL,\n" +
+ " created_date datetime NOT NULL,\n" +
+ " updated_by varchar(50) NOT NULL,\n" +
+ " updated_date datetime NOT NULL,\n" +
+ " account_record_id int(11) unsigned default null,\n" +
+ " tenant_record_id int(11) unsigned default null,\n" +
+ " PRIMARY KEY (record_id)\n" +
+ ");");
+
+ for (final String pack : new String[]{"account", "analytics", "beatrix", "subscription", "util", "payment", "invoice", "entitlement", "usage", "meter", "tenant"}) {
+ for (final String ddlFile : new String[]{"ddl.sql", "ddl_test.sql"}) {
+ final String ddl;
+ try {
+ ddl = IOUtils.toString(Resources.getResource("org/killbill/billing/" + pack + "/" + ddlFile).openStream());
+ } catch (final IllegalArgumentException ignored) {
+ // The test doesn't have this module ddl in the classpath - that's fine
+ continue;
+ }
+ instance.executeScript(ddl);
+ }
+ }
+ instance.refreshTableNames();
+ }
+
+ private static boolean isUsingLocalInstance() {
+ return (System.getProperty("org.killbill.billing.dbi.test.useLocalDb") != null);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestModule.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestModule.java
new file mode 100644
index 0000000..00c37ea
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestModule.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing;
+
+import java.util.UUID;
+
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
+
+import com.google.inject.AbstractModule;
+
+public class GuicyKillbillTestModule extends AbstractModule {
+
+ //
+ // CreatedFontTracker references that will later be injected through Guices.
+ // That we we have only one clock and all internalContext/callcontext are consistent
+ //
+
+ private final InternalCallContext internalCallContext = new InternalCallContext(InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID, 1687L, UUID.randomUUID(),
+ UUID.randomUUID().toString(), CallOrigin.TEST,
+ UserType.TEST, "Testing", "This is a test",
+ GuicyKillbillTestSuite.getClock().getUTCNow(), GuicyKillbillTestSuite.getClock().getUTCNow());
+
+ private final CallContext callContext = internalCallContext.toCallContext(null);
+
+
+
+ @Override
+ protected void configure() {
+ bind(ClockMock.class).toInstance(GuicyKillbillTestSuite.getClock());
+ bind(Clock.class).to(ClockMock.class);
+ bind(InternalCallContext.class).toInstance(internalCallContext);
+ bind(CallContext.class).toInstance(callContext);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestNoDBModule.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestNoDBModule.java
new file mode 100644
index 0000000..82319ef
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestNoDBModule.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing;
+
+import org.mockito.Mockito;
+import org.skife.jdbi.v2.IDBI;
+
+public class GuicyKillbillTestNoDBModule extends GuicyKillbillTestModule {
+
+ private void installDBI() {
+ final IDBI idbi = Mockito.mock(IDBI.class);
+ bind(IDBI.class).toInstance(idbi);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ installDBI();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuite.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuite.java
new file mode 100644
index 0000000..d9ebc99
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuite.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing;
+
+import java.lang.reflect.Method;
+
+import javax.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.ITestResult;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.clock.ClockMock;
+
+public class GuicyKillbillTestSuite {
+
+
+ // Use the simple name here to save screen real estate
+ protected static final Logger log = LoggerFactory.getLogger(KillbillTestSuite.class.getSimpleName());
+
+ private boolean hasFailed = false;
+
+ @Inject
+ protected InternalCallContext internalCallContext;
+
+ @Inject
+ protected CallContext callContext;
+
+ @Inject
+ protected ClockMock clock;
+
+
+ private static final ClockMock theStaticClock = new ClockMock();
+
+ protected final KillbillConfigSource configSource = new KillbillConfigSource();
+
+ public GuicyKillbillTestSuite() {
+ // Ignore ehcache checks. Unfortunately, ehcache looks at system properties directly...
+ System.setProperty("net.sf.ehcache.skipUpdateCheck", "true");
+ }
+
+ public static ClockMock getClock() {
+ return theStaticClock;
+ }
+
+ @BeforeMethod(alwaysRun = true)
+ public void beforeMethodAlwaysRun(final Method method) throws Exception {
+ log.info("***************************************************************************************************");
+ log.info("*** Starting test {}:{}", method.getDeclaringClass().getName(), method.getName());
+ log.info("***************************************************************************************************");
+ }
+
+ @AfterMethod(alwaysRun = true)
+ public void afterMethodAlwaysRun(final Method method, final ITestResult result) throws Exception {
+ log.info("***************************************************************************************************");
+ log.info("*** Ending test {}:{} {} ({} s.)", new Object[]{method.getDeclaringClass().getName(), method.getName(),
+ result.isSuccess() ? "SUCCESS" : "!!! FAILURE !!!",
+ (result.getEndMillis() - result.getStartMillis()) / 1000});
+ log.info("***************************************************************************************************");
+ if (!hasFailed && !result.isSuccess()) {
+ hasFailed = true;
+ }
+ }
+
+ public boolean hasFailed() {
+ return hasFailed;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteNoDB.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteNoDB.java
new file mode 100644
index 0000000..734aaef
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteNoDB.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing;
+
+public class GuicyKillbillTestSuiteNoDB extends GuicyKillbillTestSuite {
+
+
+}
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..29516c0
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing;
+
+import javax.inject.Inject;
+import javax.sql.DataSource;
+
+import org.skife.jdbi.v2.IDBI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterSuite;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeSuite;
+
+import org.killbill.commons.embeddeddb.EmbeddedDB;
+
+public class GuicyKillbillTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite {
+
+ private static final Logger log = LoggerFactory.getLogger(GuicyKillbillTestSuiteWithEmbeddedDB.class);
+
+ @Inject
+ protected EmbeddedDB helper;
+
+ @Inject
+ protected DataSource dataSource;
+
+ @Inject
+ protected IDBI dbi;
+
+ @BeforeSuite(groups = "slow")
+ public void beforeSuite() throws Exception {
+ DBTestingHelper.start();
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ try {
+ DBTestingHelper.get().cleanupAllTables();
+ } catch (final Exception ignored) {
+ }
+ }
+
+ @AfterSuite(groups = "slow")
+ public void afterSuite() throws Exception {
+ if (hasFailed()) {
+ log.error("**********************************************************************************************");
+ log.error("*** TESTS HAVE FAILED - LEAVING DB RUNNING FOR DEBUGGING - MAKE SURE TO KILL IT ONCE DONE ****");
+ log.error(DBTestingHelper.get().getCmdLineConnectionString());
+ log.error("**********************************************************************************************");
+ return;
+ }
+
+ try {
+ DBTestingHelper.get().stop();
+ } catch (final Exception ignored) {
+ }
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java b/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java
new file mode 100644
index 0000000..d459d87
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/GuicyKillbillTestWithEmbeddedDBModule.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing;
+
+import java.io.IOException;
+
+import javax.sql.DataSource;
+
+import org.skife.jdbi.v2.IDBI;
+import org.testng.Assert;
+
+import org.killbill.commons.embeddeddb.EmbeddedDB;
+
+public class GuicyKillbillTestWithEmbeddedDBModule extends GuicyKillbillTestModule {
+
+ @Override
+ protected void configure() {
+ super.configure();
+
+ final EmbeddedDB instance = DBTestingHelper.get();
+ bind(EmbeddedDB.class).toInstance(instance);
+
+ try {
+ bind(DataSource.class).toInstance(DBTestingHelper.get().getDataSource());
+ bind(IDBI.class).toInstance(DBTestingHelper.getDBI());
+ } catch (final IOException e) {
+ Assert.fail(e.toString());
+ }
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/KillbillConfigSource.java b/util/src/test/java/org/killbill/billing/KillbillConfigSource.java
new file mode 100644
index 0000000..65789a2
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/KillbillConfigSource.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Properties;
+
+import org.skife.config.ConfigSource;
+
+public class KillbillConfigSource implements ConfigSource {
+
+ private final Properties properties;
+
+ public KillbillConfigSource() {
+ this(System.getProperties());
+ }
+
+ public KillbillConfigSource(final Properties properties) {
+ this.properties = new Properties(properties);
+ this.properties.put("user.timezone", "UTC");
+
+ // Speed up the notification queue
+ this.properties.put("org.killbill.notificationq.main.sleep", "100");
+ // Speed up the bus
+ this.properties.put("org.killbill.persistent.bus.main.sleep", "100");
+ this.properties.put("org.killbill.persistent.bus.main.nbThreads", "1");
+ }
+
+ public String getString(final String propertyName) {
+ return properties.getProperty(propertyName);
+ }
+
+ public void merge(final URL url) {
+ final Properties properties = new Properties();
+ try {
+ properties.load(url.openStream());
+ for (final String propertyName : properties.stringPropertyNames()) {
+ setProperty(propertyName, properties.getProperty(propertyName));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void setProperty(final String propertyName, final Object propertyValue) {
+ properties.put(propertyName, propertyValue);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/KillbillTestSuite.java b/util/src/test/java/org/killbill/billing/KillbillTestSuite.java
new file mode 100644
index 0000000..7af49d2
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/KillbillTestSuite.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing;
+
+import java.lang.reflect.Method;
+import java.util.UUID;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.ITestResult;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
+
+public class KillbillTestSuite {
+
+ // Use the simple name here to save screen real estate
+ protected static final Logger log = LoggerFactory.getLogger(KillbillTestSuite.class.getSimpleName());
+
+ private boolean hasFailed = false;
+
+ protected Clock clock = new ClockMock();
+
+ protected final InternalCallContext internalCallContext = new InternalCallContext(InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID, 1687L, UUID.randomUUID(),
+ UUID.randomUUID().toString(), CallOrigin.TEST,
+ UserType.TEST, "Testing", "This is a test",
+ clock.getUTCNow(), clock.getUTCNow());
+ protected final CallContext callContext = internalCallContext.toCallContext(null);
+
+ @BeforeMethod(alwaysRun = true)
+ public void startTestSuite(final Method method) throws Exception {
+ log.info("***************************************************************************************************");
+ log.info("*** Starting test {}:{}", method.getDeclaringClass().getName(), method.getName());
+ log.info("***************************************************************************************************");
+ }
+
+ @AfterMethod(alwaysRun = true)
+ public void endTestSuite(final Method method, final ITestResult result) throws Exception {
+ log.info("***************************************************************************************************");
+ log.info("*** Ending test {}:{} {} ({} s.)", new Object[]{method.getDeclaringClass().getName(), method.getName(),
+ result.isSuccess() ? "SUCCESS" : "!!! FAILURE !!!",
+ (result.getEndMillis() - result.getStartMillis()) / 1000});
+ log.info("***************************************************************************************************");
+ if (!hasFailed && !result.isSuccess()) {
+ hasFailed = true;
+ }
+ }
+
+ public boolean hasFailed() {
+ return hasFailed;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/KillbillTestSuiteWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/KillbillTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..14a2123
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/KillbillTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.sql.SQLException;
+
+import org.skife.jdbi.v2.IDBI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterSuite;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeSuite;
+
+public class KillbillTestSuiteWithEmbeddedDB extends KillbillTestSuite {
+
+ private static final Logger log = LoggerFactory.getLogger(KillbillTestSuiteWithEmbeddedDB.class);
+
+ @BeforeSuite(groups = "slow")
+ public void startMysqlBeforeTestSuite() throws IOException, ClassNotFoundException, SQLException, URISyntaxException {
+ DBTestingHelper.start();
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void cleanupTablesBetweenMethods() {
+ try {
+ DBTestingHelper.get().cleanupAllTables();
+ } catch (final Exception ignored) {
+ }
+ }
+
+ @AfterSuite(groups = "slow")
+ public void shutdownMysqlAfterTestSuite() throws IOException, ClassNotFoundException, SQLException, URISyntaxException {
+ if (hasFailed()) {
+ log.error("**********************************************************************************************");
+ log.error("*** TESTS HAVE FAILED - LEAVING DB RUNNING FOR DEBUGGING - MAKE SURE TO KILL IT ONCE DONE ****");
+ log.error(DBTestingHelper.get().getCmdLineConnectionString());
+ log.error("**********************************************************************************************");
+ return;
+ }
+
+ try {
+ DBTestingHelper.get().stop();
+ } catch (final Exception ignored) {
+ }
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/api/MockAccountUserApi.java b/util/src/test/java/org/killbill/billing/mock/api/MockAccountUserApi.java
new file mode 100644
index 0000000..d365b82
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/api/MockAccountUserApi.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.api;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.joda.time.DateTimeZone;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.account.api.AccountEmail;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.mock.MockAccountBuilder;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.DefaultPagination;
+import org.killbill.billing.util.entity.Pagination;
+
+public class MockAccountUserApi implements AccountUserApi {
+
+ private final CopyOnWriteArrayList<Account> accounts = new CopyOnWriteArrayList<Account>();
+
+ public Account createAccountFromParams(final UUID id,
+ final String externalKey,
+ final String email,
+ final String name,
+ final int firstNameLength,
+ final Currency currency,
+ final int billCycleDayLocal,
+ final UUID paymentMethodId,
+ final DateTimeZone timeZone,
+ final String locale,
+ final String address1,
+ final String address2,
+ final String companyName,
+ final String city,
+ final String stateOrProvince,
+ final String country,
+ final String postalCode,
+ final String phone) {
+ final Account result = new MockAccountBuilder(id)
+ .externalKey(externalKey)
+ .email(email)
+ .name(name).firstNameLength(firstNameLength)
+ .currency(currency)
+ .billingCycleDayLocal(billCycleDayLocal)
+ .paymentMethodId(paymentMethodId)
+ .timeZone(timeZone)
+ .locale(locale)
+ .address1(address1)
+ .address2(address2)
+ .companyName(companyName)
+ .city(city)
+ .stateOrProvince(stateOrProvince)
+ .country(country)
+ .postalCode(postalCode)
+ .phone(phone)
+ .isNotifiedForInvoices(false)
+ .build();
+ accounts.add(result);
+ return result;
+ }
+
+ @Override
+ public Account createAccount(final AccountData data, final CallContext context) throws AccountApiException {
+ final Account result = new MockAccountBuilder(data).build();
+ accounts.add(result);
+ return result;
+ }
+
+ @Override
+ public Account getAccountByKey(final String key, final TenantContext context) {
+ for (final Account account : accounts) {
+ if (key.equals(account.getExternalKey())) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Account getAccountById(final UUID uid, final TenantContext context) {
+ for (final Account account : accounts) {
+ if (uid.equals(account.getId())) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Pagination<Account> searchAccounts(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) {
+ final List<Account> results = new LinkedList<Account>();
+ for (final Account account : accounts) {
+ if ((account.getName() != null && account.getName().contains(searchKey)) ||
+ (account.getEmail() != null && account.getEmail().contains(searchKey)) ||
+ (account.getExternalKey() != null && account.getExternalKey().contains(searchKey)) ||
+ (account.getCompanyName() != null && account.getCompanyName().contains(searchKey))) {
+ results.add(account);
+ }
+ }
+ return DefaultPagination.<Account>build(offset, limit, results);
+ }
+
+ @Override
+ public Pagination<Account> getAccounts(final Long offset, final Long limit, final TenantContext context) {
+ return DefaultPagination.<Account>build(offset, limit, accounts);
+ }
+
+ @Override
+ public UUID getIdFromKey(final String externalKey, final TenantContext context) {
+ for (final Account account : accounts) {
+ if (externalKey.equals(account.getExternalKey())) {
+ return account.getId();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public List<AccountEmail> getEmails(final UUID accountId, final TenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeEmail(final UUID accountId, final AccountEmail email, final CallContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void updateAccount(final Account account, final CallContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void updateAccount(final String key, final AccountData accountData, final CallContext context)
+ throws AccountApiException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void updateAccount(final UUID accountId, final AccountData accountData, final CallContext context)
+ throws AccountApiException {
+ throw new UnsupportedOperationException();
+ }
+}
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
new file mode 100644
index 0000000..206ff36
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/api/MockExtBusEvent.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.api;
+
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.notification.plugin.api.ExtBusEvent;
+import org.killbill.billing.notification.plugin.api.ExtBusEventType;
+
+/**
+ * Used for Jruby plugin that import util test package for default implementation of interfaces in api.
+ * So despite the appearences, this class is used.
+ */
+public class MockExtBusEvent implements ExtBusEvent {
+
+ private final ExtBusEventType eventType;
+ private final ObjectType objectType;
+ private final UUID objectId;
+ private final UUID accountId;
+ private final UUID tenantId;
+
+
+ public MockExtBusEvent(final ExtBusEventType eventType,
+ final ObjectType objectType,
+ final UUID objectId,
+ final UUID accountId,
+ final UUID tenantId) {
+ this.eventType = eventType;
+ this.objectId = objectId;
+ this.objectType = objectType;
+ this.accountId = accountId;
+ this.tenantId = tenantId;
+ }
+
+ @Override
+ public ExtBusEventType getEventType() {
+ return eventType;
+ }
+
+ @Override
+ public ObjectType getObjectType() {
+ return objectType;
+ }
+
+ @Override
+ public UUID getObjectId() {
+ return objectId;
+ }
+
+ @Override
+ public UUID getAccountId() {
+ return accountId;
+ }
+
+ @Override
+ public UUID getTenantId() {
+ return tenantId;
+ }
+
+ @Override
+ public String toString() {
+ return "MockExtBusEvent [eventType=" + eventType + ", objectType="
+ + objectType + ", objectId=" + objectId + ", accountId="
+ + accountId + ", tenantId=" + tenantId + "]";
+ }
+
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockAccountModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockAccountModule.java
new file mode 100644
index 0000000..919760e
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockAccountModule.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import org.mockito.Mockito;
+
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.glue.AccountModule;
+import org.killbill.billing.account.api.AccountInternalApi;
+
+import com.google.inject.AbstractModule;
+
+public class MockAccountModule extends AbstractModule implements AccountModule {
+
+ @Override
+ protected void configure() {
+ installAccountUserApi();
+ installInternalApi();
+ }
+
+
+ @Override
+ public void installAccountUserApi() {
+ bind(AccountUserApi.class).toInstance(Mockito.mock(AccountUserApi.class));
+ }
+
+ @Override
+ public void installInternalApi() {
+ bind(AccountInternalApi.class).toInstance(Mockito.mock(AccountInternalApi.class));
+ }
+
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockClockModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockClockModule.java
new file mode 100644
index 0000000..a0a3f32
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockClockModule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import org.killbill.clock.Clock;
+import org.killbill.clock.ClockMock;
+
+import com.google.inject.AbstractModule;
+
+
+public class MockClockModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(Clock.class).to(ClockMock.class).asEagerSingleton();
+ bind(ClockMock.class).asEagerSingleton();
+ }
+
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockEntitlementModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockEntitlementModule.java
new file mode 100644
index 0000000..6c7c927
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockEntitlementModule.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import org.mockito.Mockito;
+
+import org.killbill.billing.entitlement.EntitlementInternalApi;
+import org.killbill.billing.entitlement.api.EntitlementApi;
+import org.killbill.billing.entitlement.api.SubscriptionApi;
+import org.killbill.billing.glue.EntitlementModule;
+import org.killbill.billing.junction.BlockingInternalApi;
+
+import com.google.inject.AbstractModule;
+
+public class MockEntitlementModule extends AbstractModule implements EntitlementModule {
+
+ private final BlockingInternalApi blockingApi = Mockito.mock(BlockingInternalApi.class);
+ private final EntitlementApi entitlementApi = Mockito.mock(EntitlementApi.class);
+ private final EntitlementInternalApi entitlementInternalApi = Mockito.mock(EntitlementInternalApi.class);
+ private final SubscriptionApi subscriptionApi = Mockito.mock(SubscriptionApi.class);
+
+ @Override
+ protected void configure() {
+ installBlockingStateDao();
+ installBlockingApi();
+ installEntitlementApi();
+ installBlockingChecker();
+ }
+
+ @Override
+ public void installBlockingStateDao() {
+ }
+
+ @Override
+ public void installBlockingApi() {
+ //bind(BlockingInternalApi.class).toInstance(blockingApi);
+ }
+
+ @Override
+ public void installEntitlementApi() {
+ bind(EntitlementApi.class).toInstance(entitlementApi);
+ }
+
+ @Override
+ public void installEntitlementInternalApi() {
+ bind(EntitlementInternalApi.class).toInstance(entitlementInternalApi);
+ }
+
+ @Override
+ public void installSubscriptionApi() {
+ bind(SubscriptionApi.class).toInstance(subscriptionApi);
+ }
+
+ @Override
+ public void installBlockingChecker() {
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockGlobalLockerModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockGlobalLockerModule.java
new file mode 100644
index 0000000..ca9ec95
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockGlobalLockerModule.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.commons.locker.memory.MemoryGlobalLocker;
+
+import com.google.inject.AbstractModule;
+
+public class MockGlobalLockerModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(GlobalLocker.class).to(MemoryGlobalLocker.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockInvoiceModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockInvoiceModule.java
new file mode 100644
index 0000000..f1b25ef
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockInvoiceModule.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import org.mockito.Mockito;
+
+import org.killbill.billing.glue.InvoiceModule;
+import org.killbill.billing.invoice.api.InvoiceMigrationApi;
+import org.killbill.billing.invoice.api.InvoicePaymentApi;
+import org.killbill.billing.invoice.api.InvoiceUserApi;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+
+import com.google.inject.AbstractModule;
+
+public class MockInvoiceModule extends AbstractModule implements InvoiceModule {
+
+ @Override
+ public void installInvoiceUserApi() {
+ bind(InvoiceUserApi.class).toInstance(Mockito.mock(InvoiceUserApi.class));
+ }
+
+ @Override
+ public void installInvoicePaymentApi() {
+ bind(InvoicePaymentApi.class).toInstance(Mockito.mock(InvoicePaymentApi.class));
+ }
+
+ @Override
+ public void installInvoiceMigrationApi() {
+ bind(InvoiceMigrationApi.class).toInstance(Mockito.mock(InvoiceMigrationApi.class));
+ }
+
+ @Override
+ protected void configure() {
+ installInvoiceUserApi();
+ installInvoiceInternalApi();
+ installInvoicePaymentApi();
+ installInvoiceMigrationApi();
+ }
+
+ @Override
+ public void installInvoiceInternalApi() {
+ bind(InvoiceInternalApi.class).toInstance(Mockito.mock(InvoiceInternalApi.class));
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockJunctionModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockJunctionModule.java
new file mode 100644
index 0000000..fac5a03
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockJunctionModule.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import com.google.inject.AbstractModule;
+import org.killbill.billing.glue.JunctionModule;
+import org.killbill.billing.junction.BillingInternalApi;
+import org.mockito.Mockito;
+
+public class MockJunctionModule extends AbstractModule implements JunctionModule {
+ private final BillingInternalApi billingApi = Mockito.mock(BillingInternalApi.class);
+
+ @Override
+ protected void configure() {
+ installBillingApi();
+ }
+
+ @Override
+ public void installBillingApi() {
+ bind(BillingInternalApi.class).toInstance(billingApi);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockNonEntityDaoModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockNonEntityDaoModule.java
new file mode 100644
index 0000000..d71574a
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockNonEntityDaoModule.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.dao.MockNonEntityDao;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.dao.TableName;
+
+import com.google.inject.AbstractModule;
+
+public class MockNonEntityDaoModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(NonEntityDao.class).to(MockNonEntityDao.class);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockNotificationQueueModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockNotificationQueueModule.java
new file mode 100644
index 0000000..8e92d48
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockNotificationQueueModule.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
+import org.killbill.notificationq.MockNotificationQueueService;
+import org.killbill.notificationq.api.NotificationQueueConfig;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.util.glue.NotificationQueueModule;
+
+import com.google.common.collect.ImmutableMap;
+
+public class MockNotificationQueueModule extends NotificationQueueModule {
+
+ public MockNotificationQueueModule(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configureNotificationQueueConfig() {
+ final NotificationQueueConfig config = new ConfigurationObjectFactory(configSource).buildWithReplacements(NotificationQueueConfig.class,
+ ImmutableMap.<String, String>of("instanceName", "main"));
+ bind(NotificationQueueConfig.class).toInstance(config);
+ }
+
+ @Override
+ protected void configure() {
+ bind(NotificationQueueService.class).to(MockNotificationQueueService.class).asEagerSingleton();
+ configureNotificationQueueConfig();
+ }
+
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockOverdueModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockOverdueModule.java
new file mode 100644
index 0000000..91fc45f
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockOverdueModule.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import org.mockito.Mockito;
+
+import org.killbill.billing.glue.OverdueModule;
+import org.killbill.billing.overdue.OverdueUserApi;
+
+import com.google.inject.AbstractModule;
+
+public class MockOverdueModule extends AbstractModule implements OverdueModule {
+ @Override
+ public void installOverdueUserApi() {
+ bind(OverdueUserApi.class).toInstance(Mockito.mock(OverdueUserApi.class));
+ }
+
+ @Override
+ protected void configure() {
+ installOverdueUserApi();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockPaymentModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockPaymentModule.java
new file mode 100644
index 0000000..e4af15f
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockPaymentModule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import org.mockito.Mockito;
+
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentInternalApi;
+
+import com.google.inject.AbstractModule;
+
+public class MockPaymentModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(PaymentApi.class).toInstance(Mockito.mock(PaymentApi.class));
+ bind(PaymentInternalApi.class).toInstance(Mockito.mock(PaymentInternalApi.class));
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockSubscriptionModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockSubscriptionModule.java
new file mode 100644
index 0000000..ddf4617
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockSubscriptionModule.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import org.mockito.Mockito;
+
+import org.killbill.billing.glue.SubscriptionModule;
+import org.killbill.billing.subscription.api.SubscriptionBaseService;
+import org.killbill.billing.subscription.api.migration.SubscriptionBaseMigrationApi;
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineApi;
+import org.killbill.billing.subscription.api.transfer.SubscriptionBaseTransferApi;
+import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
+
+import com.google.inject.AbstractModule;
+
+public class MockSubscriptionModule extends AbstractModule implements SubscriptionModule {
+
+ @Override
+ public void installSubscriptionService() {
+ bind(SubscriptionBaseService.class).toInstance(Mockito.mock(SubscriptionBaseService.class));
+ }
+
+
+ @Override
+ public void installSubscriptionMigrationApi() {
+ bind(SubscriptionBaseMigrationApi.class).toInstance(Mockito.mock(SubscriptionBaseMigrationApi.class));
+ }
+
+ @Override
+ public void installSubscriptionInternalApi() {
+ bind(SubscriptionBaseInternalApi.class).toInstance(Mockito.mock(SubscriptionBaseInternalApi.class));
+ }
+
+ @Override
+ protected void configure() {
+ installSubscriptionService();
+ installSubscriptionMigrationApi();
+ installSubscriptionInternalApi();
+ installSubscriptionTimelineApi();
+ installSubscriptionTransferApi();
+ }
+
+ @Override
+ public void installSubscriptionTimelineApi() {
+ bind(SubscriptionBaseTimelineApi.class).toInstance(Mockito.mock(SubscriptionBaseTimelineApi.class));
+ }
+
+ @Override
+ public void installSubscriptionTransferApi() {
+ bind(SubscriptionBaseTransferApi.class).toInstance(Mockito.mock(SubscriptionBaseTransferApi.class));
+
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockTagModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockTagModule.java
new file mode 100644
index 0000000..b9e7e01
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockTagModule.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock.glue;
+
+import org.killbill.billing.util.glue.TagStoreModule;
+import org.killbill.billing.util.tag.dao.MockTagDao;
+import org.killbill.billing.util.tag.dao.MockTagDefinitionDao;
+import org.killbill.billing.util.tag.dao.TagDao;
+import org.killbill.billing.util.tag.dao.TagDefinitionDao;
+
+public class MockTagModule extends TagStoreModule {
+
+ @Override
+ protected void installDaos() {
+ bind(TagDefinitionDao.class).to(MockTagDefinitionDao.class).asEagerSingleton();
+ bind(TagDao.class).to(MockTagDao.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockAccountBuilder.java b/util/src/test/java/org/killbill/billing/mock/MockAccountBuilder.java
new file mode 100644
index 0000000..65a54d9
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/MockAccountBuilder.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.account.api.MutableAccountData;
+import org.killbill.billing.catalog.api.Currency;
+
+public class MockAccountBuilder {
+
+ private final UUID id;
+ private String externalKey = "";
+ private String email = "";
+ private String name = "";
+ private int firstNameLength;
+ private Currency currency = Currency.USD;
+ private int billingCycleDayLocal;
+ private UUID paymentMethodId;
+ private DateTimeZone timeZone = DateTimeZone.UTC;
+ private String locale = "";
+ private String address1 = "";
+ private String address2 = "";
+ private String companyName = "";
+ private String city = "";
+ private String stateOrProvince = "";
+ private String country = "";
+ private String postalCode = "";
+ private String phone = "";
+ private boolean migrated;
+ private boolean isNotifiedForInvoices;
+ private DateTime createdDate = new DateTime(DateTimeZone.UTC);
+ private DateTime updatedDate = new DateTime(DateTimeZone.UTC);
+
+ public MockAccountBuilder() {
+ this(UUID.randomUUID());
+ }
+
+ public MockAccountBuilder(final UUID id) {
+ this.id = id;
+ }
+
+ public MockAccountBuilder(final AccountData data) {
+ this.id = UUID.randomUUID();
+ this.address1(data.getAddress1());
+ this.address2(data.getAddress2());
+ this.billingCycleDayLocal(data.getBillCycleDayLocal());
+ this.city(data.getCity());
+ this.companyName(data.getCompanyName());
+ this.country(data.getCountry());
+ this.currency(data.getCurrency());
+ this.email(data.getEmail());
+ this.externalKey(data.getExternalKey());
+ this.firstNameLength(data.getFirstNameLength());
+ this.isNotifiedForInvoices(data.isNotifiedForInvoices());
+ this.locale(data.getLocale());
+ this.migrated(data.isMigrated());
+ this.name(data.getName());
+ this.paymentMethodId(data.getPaymentMethodId());
+ this.phone(data.getPhone());
+ this.postalCode(data.getPostalCode());
+ this.stateOrProvince(data.getStateOrProvince());
+ this.timeZone(data.getTimeZone());
+ }
+
+ public MockAccountBuilder externalKey(final String externalKey) {
+ this.externalKey = externalKey;
+ return this;
+ }
+
+ public MockAccountBuilder email(final String email) {
+ this.email = email;
+ return this;
+ }
+
+ public MockAccountBuilder name(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ public MockAccountBuilder firstNameLength(final int firstNameLength) {
+ this.firstNameLength = firstNameLength;
+ return this;
+ }
+
+ public MockAccountBuilder billingCycleDayLocal(final int billingCycleDayLocal) {
+ this.billingCycleDayLocal = billingCycleDayLocal;
+ return this;
+ }
+
+ public MockAccountBuilder currency(final Currency currency) {
+ this.currency = currency;
+ return this;
+ }
+
+ public MockAccountBuilder paymentMethodId(final UUID paymentMethodId) {
+ this.paymentMethodId = paymentMethodId;
+ return this;
+ }
+
+ public MockAccountBuilder timeZone(final DateTimeZone timeZone) {
+ this.timeZone = timeZone;
+ return this;
+ }
+
+ public MockAccountBuilder locale(final String locale) {
+ this.locale = locale;
+ return this;
+ }
+
+ public MockAccountBuilder address1(final String address1) {
+ this.address1 = address1;
+ return this;
+ }
+
+ public MockAccountBuilder address2(final String address2) {
+ this.address2 = address2;
+ return this;
+ }
+
+ public MockAccountBuilder companyName(final String companyName) {
+ this.companyName = companyName;
+ return this;
+ }
+
+ public MockAccountBuilder city(final String city) {
+ this.city = city;
+ return this;
+ }
+
+ public MockAccountBuilder stateOrProvince(final String stateOrProvince) {
+ this.stateOrProvince = stateOrProvince;
+ return this;
+ }
+
+ public MockAccountBuilder postalCode(final String postalCode) {
+ this.postalCode = postalCode;
+ return this;
+ }
+
+ public MockAccountBuilder country(final String country) {
+ this.country = country;
+ return this;
+ }
+
+ public MockAccountBuilder phone(final String phone) {
+ this.phone = phone;
+ return this;
+ }
+
+ public MockAccountBuilder migrated(final boolean migrated) {
+ this.migrated = migrated;
+ return this;
+ }
+
+ public MockAccountBuilder isNotifiedForInvoices(final boolean isNotifiedForInvoices) {
+ this.isNotifiedForInvoices = isNotifiedForInvoices;
+ return this;
+ }
+
+ public MockAccountBuilder createdDate(final DateTime createdDate) {
+ this.createdDate = createdDate;
+ return this;
+ }
+
+ public MockAccountBuilder updatedDate(final DateTime updatedDate) {
+ this.updatedDate = updatedDate;
+ return this;
+ }
+
+ public Account build() {
+ return new Account() {
+ @Override
+ public DateTime getCreatedDate() {
+ return createdDate;
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return updatedDate;
+ }
+
+ @Override
+ public String getExternalKey() {
+ return externalKey;
+ }
+
+ @Override
+ public String getName() {
+
+ return name;
+ }
+
+ @Override
+ public Integer getFirstNameLength() {
+
+ return firstNameLength;
+ }
+
+ @Override
+ public String getEmail() {
+
+ return email;
+ }
+
+ @Override
+ public Integer getBillCycleDayLocal() {
+
+ return billingCycleDayLocal;
+ }
+
+ @Override
+ public Currency getCurrency() {
+
+ return currency;
+ }
+
+ @Override
+ public UUID getPaymentMethodId() {
+
+ return paymentMethodId;
+ }
+
+ @Override
+ public DateTimeZone getTimeZone() {
+
+ return timeZone;
+ }
+
+ @Override
+ public String getLocale() {
+
+ return locale;
+ }
+
+ @Override
+ public String getAddress1() {
+
+ return address1;
+ }
+
+ @Override
+ public String getAddress2() {
+
+ return address2;
+ }
+
+ @Override
+ public String getCompanyName() {
+
+ return companyName;
+ }
+
+ @Override
+ public String getCity() {
+
+ return city;
+ }
+
+ @Override
+ public String getStateOrProvince() {
+
+ return stateOrProvince;
+ }
+
+ @Override
+ public String getPostalCode() {
+
+ return postalCode;
+ }
+
+ @Override
+ public String getCountry() {
+
+ return country;
+ }
+
+ @Override
+ public String getPhone() {
+
+ return phone;
+ }
+
+ @Override
+ public Boolean isMigrated() {
+
+ return migrated;
+ }
+
+ @Override
+ public Boolean isNotifiedForInvoices() {
+
+ return isNotifiedForInvoices;
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public MutableAccountData toMutableAccountData() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Account mergeWithDelegate(final Account delegate) {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java b/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java
new file mode 100644
index 0000000..793f2f0
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/MockEffectiveSubscriptionEvent.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.events.BusEventBase;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class MockEffectiveSubscriptionEvent extends BusEventBase implements EffectiveSubscriptionInternalEvent {
+
+ private final Long totalOrdering;
+ private final UUID subscriptionId;
+ private final UUID bundleId;
+ private final UUID eventId;
+ private final DateTime requestedTransitionTime;
+ private final DateTime effectiveTransitionTime;
+ private final EntitlementState previousState;
+ private final String previousPriceList;
+ private final String previousPlan;
+ private final String previousPhase;
+ private final EntitlementState nextState;
+ private final String nextPriceList;
+ private final String nextPlan;
+ private final String nextPhase;
+ private final Integer remainingEventsForUserOperation;
+ private final UUID userToken;
+ private final SubscriptionBaseTransitionType transitionType;
+
+ private final DateTime startDate;
+
+ @JsonCreator
+ public MockEffectiveSubscriptionEvent(@JsonProperty("eventId") final UUID eventId,
+ @JsonProperty("subscriptionId") final UUID subscriptionId,
+ @JsonProperty("bundleId") final UUID bundleId,
+ @JsonProperty("requestedTransitionTime") final DateTime requestedTransitionTime,
+ @JsonProperty("effectiveTransitionTime") final DateTime effectiveTransitionTime,
+ @JsonProperty("previousState") final EntitlementState previousState,
+ @JsonProperty("previousPlan") final String previousPlan,
+ @JsonProperty("previousPhase") final String previousPhase,
+ @JsonProperty("previousPriceList") final String previousPriceList,
+ @JsonProperty("nextState") final EntitlementState nextState,
+ @JsonProperty("nextPlan") final String nextPlan,
+ @JsonProperty("nextPhase") final String nextPhase,
+ @JsonProperty("nextPriceList") final String nextPriceList,
+ @JsonProperty("totalOrdering") final Long totalOrdering,
+ @JsonProperty("transitionType") final SubscriptionBaseTransitionType transitionType,
+ @JsonProperty("remainingEventsForUserOperation") final Integer remainingEventsForUserOperation,
+ @JsonProperty("startDate") final DateTime startDate,
+ @JsonProperty("searchKey1") final Long searchKey1,
+ @JsonProperty("searchKey2") final Long searchKey2,
+ @JsonProperty("userToken") final UUID userToken) {
+ super(searchKey1, searchKey2, userToken);
+ this.eventId = eventId;
+ this.subscriptionId = subscriptionId;
+ this.bundleId = bundleId;
+ this.requestedTransitionTime = requestedTransitionTime;
+ this.effectiveTransitionTime = effectiveTransitionTime;
+ this.previousState = previousState;
+ this.previousPriceList = previousPriceList;
+ this.previousPlan = previousPlan;
+ this.previousPhase = previousPhase;
+ this.nextState = nextState;
+ this.nextPlan = nextPlan;
+ this.nextPriceList = nextPriceList;
+ this.nextPhase = nextPhase;
+ this.totalOrdering = totalOrdering;
+ this.userToken = userToken;
+ this.transitionType = transitionType;
+ this.remainingEventsForUserOperation = remainingEventsForUserOperation;
+ this.startDate = startDate;
+ }
+
+ @JsonIgnore
+ @Override
+ public BusInternalEventType getBusEventType() {
+ return BusInternalEventType.SUBSCRIPTION_TRANSITION;
+ }
+
+ @JsonProperty("eventId")
+ @Override
+ public UUID getId() {
+ return eventId;
+ }
+
+ @Override
+ public UUID getSubscriptionId() {
+ return subscriptionId;
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+
+ @Override
+ public EntitlementState getPreviousState() {
+ return previousState;
+ }
+
+ @Override
+ public String getPreviousPlan() {
+ return previousPlan;
+ }
+
+ @Override
+ public String getPreviousPhase() {
+ return previousPhase;
+ }
+
+ @Override
+ public String getNextPlan() {
+ return nextPlan;
+ }
+
+ @Override
+ public String getNextPhase() {
+ return nextPhase;
+ }
+
+ @Override
+ public EntitlementState getNextState() {
+ return nextState;
+ }
+
+
+ @Override
+ public String getPreviousPriceList() {
+ return previousPriceList;
+ }
+
+ @Override
+ public String getNextPriceList() {
+ return nextPriceList;
+ }
+
+ @Override
+ public Integer getRemainingEventsForUserOperation() {
+ return remainingEventsForUserOperation;
+ }
+
+
+ @Override
+ public DateTime getRequestedTransitionTime() {
+ return requestedTransitionTime;
+ }
+
+ @Override
+ public DateTime getEffectiveTransitionTime() {
+ return effectiveTransitionTime;
+ }
+
+ @Override
+ public Long getTotalOrdering() {
+ return totalOrdering;
+ }
+
+ @Override
+ public SubscriptionBaseTransitionType getTransitionType() {
+ return transitionType;
+ }
+
+ @JsonProperty("startDate")
+ @Override
+ public DateTime getSubscriptionStartDate() {
+ return startDate;
+ }
+
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((bundleId == null) ? 0 : bundleId.hashCode());
+ result = prime
+ * result
+ + ((effectiveTransitionTime == null) ? 0
+ : effectiveTransitionTime.hashCode());
+ result = prime * result + ((eventId == null) ? 0 : eventId.hashCode());
+ result = prime * result
+ + ((nextPhase == null) ? 0 : nextPhase.hashCode());
+ result = prime * result
+ + ((nextPlan == null) ? 0 : nextPlan.hashCode());
+ result = prime * result
+ + ((nextPriceList == null) ? 0 : nextPriceList.hashCode());
+ result = prime * result
+ + ((nextState == null) ? 0 : nextState.hashCode());
+ result = prime * result
+ + ((previousPhase == null) ? 0 : previousPhase.hashCode());
+ result = prime * result
+ + ((previousPlan == null) ? 0 : previousPlan.hashCode());
+ result = prime
+ * result
+ + ((previousPriceList == null) ? 0 : previousPriceList
+ .hashCode());
+ result = prime * result
+ + ((previousState == null) ? 0 : previousState.hashCode());
+ result = prime
+ * result
+ + ((remainingEventsForUserOperation == null) ? 0
+ : remainingEventsForUserOperation.hashCode());
+ result = prime
+ * result
+ + ((requestedTransitionTime == null) ? 0
+ : requestedTransitionTime.hashCode());
+ result = prime * result
+ + ((subscriptionId == null) ? 0 : subscriptionId.hashCode());
+ result = prime * result
+ + ((totalOrdering == null) ? 0 : totalOrdering.hashCode());
+ result = prime * result
+ + ((transitionType == null) ? 0 : transitionType.hashCode());
+ result = prime * result
+ + ((userToken == null) ? 0 : userToken.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final MockEffectiveSubscriptionEvent other = (MockEffectiveSubscriptionEvent) obj;
+ if (bundleId == null) {
+ if (other.bundleId != null) {
+ return false;
+ }
+ } else if (!bundleId.equals(other.bundleId)) {
+ return false;
+ }
+ if (effectiveTransitionTime == null) {
+ if (other.effectiveTransitionTime != null) {
+ return false;
+ }
+ } else if (effectiveTransitionTime
+ .compareTo(other.effectiveTransitionTime) != 0) {
+ return false;
+ }
+ if (eventId == null) {
+ if (other.eventId != null) {
+ return false;
+ }
+ } else if (!eventId.equals(other.eventId)) {
+ return false;
+ }
+ if (nextPhase == null) {
+ if (other.nextPhase != null) {
+ return false;
+ }
+ } else if (!nextPhase.equals(other.nextPhase)) {
+ return false;
+ }
+ if (nextPlan == null) {
+ if (other.nextPlan != null) {
+ return false;
+ }
+ } else if (!nextPlan.equals(other.nextPlan)) {
+ return false;
+ }
+ if (nextPriceList == null) {
+ if (other.nextPriceList != null) {
+ return false;
+ }
+ } else if (!nextPriceList.equals(other.nextPriceList)) {
+ return false;
+ }
+ if (nextState != other.nextState) {
+ return false;
+ }
+ if (previousPhase == null) {
+ if (other.previousPhase != null) {
+ return false;
+ }
+ } else if (!previousPhase.equals(other.previousPhase)) {
+ return false;
+ }
+ if (previousPlan == null) {
+ if (other.previousPlan != null) {
+ return false;
+ }
+ } else if (!previousPlan.equals(other.previousPlan)) {
+ return false;
+ }
+ if (previousPriceList == null) {
+ if (other.previousPriceList != null) {
+ return false;
+ }
+ } else if (!previousPriceList.equals(other.previousPriceList)) {
+ return false;
+ }
+ if (previousState != other.previousState) {
+ return false;
+ }
+ if (remainingEventsForUserOperation == null) {
+ if (other.remainingEventsForUserOperation != null) {
+ return false;
+ }
+ } else if (!remainingEventsForUserOperation
+ .equals(other.remainingEventsForUserOperation)) {
+ return false;
+ }
+ if (requestedTransitionTime == null) {
+ if (other.requestedTransitionTime != null) {
+ return false;
+ }
+ } else if (requestedTransitionTime
+ .compareTo(other.requestedTransitionTime) != 0) {
+ return false;
+ }
+ if (subscriptionId == null) {
+ if (other.subscriptionId != null) {
+ return false;
+ }
+ } else if (!subscriptionId.equals(other.subscriptionId)) {
+ return false;
+ }
+ if (totalOrdering == null) {
+ if (other.totalOrdering != null) {
+ return false;
+ }
+ } else if (!totalOrdering.equals(other.totalOrdering)) {
+ return false;
+ }
+ if (transitionType != other.transitionType) {
+ return false;
+ }
+ if (userToken == null) {
+ if (other.userToken != null) {
+ return false;
+ }
+ } else if (!userToken.equals(other.userToken)) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockInvoiceFormatterFactory.java b/util/src/test/java/org/killbill/billing/mock/MockInvoiceFormatterFactory.java
new file mode 100644
index 0000000..7efaaf2
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/MockInvoiceFormatterFactory.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock;
+
+import java.util.Locale;
+
+import org.killbill.billing.currency.api.CurrencyConversionApi;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
+import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+public class MockInvoiceFormatterFactory implements InvoiceFormatterFactory {
+ @Override
+ public InvoiceFormatter createInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, CurrencyConversionApi currencyConversionApi) {
+ return null;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockPlan.java b/util/src/test/java/org/killbill/billing/mock/MockPlan.java
new file mode 100644
index 0000000..2317781
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/MockPlan.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock;
+
+import java.util.Date;
+import java.util.Iterator;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+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.Product;
+
+public class MockPlan implements Plan {
+ private final String name;
+ private final Product product;
+
+ public MockPlan() {
+ this(UUID.randomUUID().toString(), new MockProduct());
+ }
+
+ public MockPlan(final String name, final Product product) {
+ this.name = name;
+ this.product = product;
+ }
+
+ @Override
+ public PlanPhase[] getInitialPhases() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Product getProduct() {
+ return product;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Date getEffectiveDateForExistingSubscriptons() {
+ return new Date();
+ }
+
+ @Override
+ public Iterator<PlanPhase> getInitialPhaseIterator() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PlanPhase getFinalPhase() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BillingPeriod getBillingPeriod() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getPlansAllowedInBundle() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PlanPhase[] getAllPhases() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PlanPhase findPhase(final String name) throws CatalogApiException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isRetired() {
+ return false;
+ }
+
+ @Override
+ public DateTime dateOfFirstRecurringNonZeroCharge(final DateTime subscriptionStartDate, PhaseType initialPhaseType) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockPriceList.java b/util/src/test/java/org/killbill/billing/mock/MockPriceList.java
new file mode 100644
index 0000000..0cbe34f
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/MockPriceList.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock;
+
+import java.util.UUID;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.Product;
+
+public class MockPriceList implements PriceList {
+ private final String name;
+ private final Boolean isRetired;
+ private final Plan plan;
+
+ public MockPriceList() {
+ this(false, UUID.randomUUID().toString(), new MockPlan());
+ }
+
+ public MockPriceList(final Boolean retired, final String name, final Plan plan) {
+ isRetired = retired;
+ this.name = name;
+ this.plan = plan;
+ }
+
+ @Override
+ public boolean isRetired() {
+ return isRetired;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Plan findPlan(final Product product, final BillingPeriod period) {
+ return plan;
+ }
+
+ public Plan getPlan() {
+ return plan;
+ }
+
+ @Override
+ public Plan[] getPlans() {
+ return new Plan[] { plan };
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockProduct.java b/util/src/test/java/org/killbill/billing/mock/MockProduct.java
new file mode 100644
index 0000000..3eec119
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/MockProduct.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock;
+
+import org.killbill.billing.catalog.api.Limit;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.ProductCategory;
+
+public class MockProduct implements Product {
+
+ private final String name;
+ private final ProductCategory category;
+ private final String catalogName;
+ private final Product[] included;
+ private final Product[] available;
+
+ public MockProduct() {
+ this("TestProduct", ProductCategory.BASE, "Vehicules");
+ }
+
+ public MockProduct(final String name, final ProductCategory category, final String catalogName) {
+ this(name, category, catalogName, null, null);
+ }
+
+ public MockProduct(final String name, final ProductCategory category, final String catalogName, final Product[] included, final Product[] available) {
+ this.name = name;
+ this.category = category;
+ this.catalogName = catalogName;
+ this.included = included;
+ this.available = available;
+ }
+
+ @Override
+ public String getCatalogName() {
+ return catalogName;
+ }
+
+ @Override
+ public ProductCategory getCategory() {
+ return category;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean isRetired() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Product[] getAvailable() {
+ return available;
+ }
+
+ @Override
+ public Product[] getIncluded() {
+ return included;
+ }
+
+ public static MockProduct createBicycle() {
+ return new MockProduct("Bicycle", ProductCategory.BASE, "Vehcles");
+ }
+
+ public static MockProduct createPickup() {
+ return new MockProduct("Pickup", ProductCategory.BASE, "Vehcles");
+ }
+
+ public static MockProduct createSportsCar() {
+ return new MockProduct("SportsCar", ProductCategory.BASE, "Vehcles");
+ }
+
+ public static MockProduct createJet() {
+ return new MockProduct("Jet", ProductCategory.BASE, "Vehcles");
+ }
+
+ public static MockProduct createHorn() {
+ return new MockProduct("Horn", ProductCategory.ADD_ON, "Vehcles");
+ }
+
+ public static MockProduct createSpotlight() {
+ return new MockProduct("spotlight", ProductCategory.ADD_ON, "Vehcles");
+ }
+
+ public static MockProduct createRedPaintJob() {
+ return new MockProduct("RedPaintJob", ProductCategory.ADD_ON, "Vehcles");
+ }
+
+ public static Product[] createAll() {
+ return new MockProduct[]{
+ createBicycle(),
+ createPickup(),
+ createSportsCar(),
+ createJet(),
+ createHorn(),
+ createRedPaintJob()
+ };
+ }
+
+ @Override
+ public Limit[] getLimits() {
+ return new Limit[0];
+ }
+
+ @Override
+ public boolean compliesWithLimits(String unit, double value) {
+ return false;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
new file mode 100644
index 0000000..5817928
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.mock;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+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.PriceList;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementSourceType;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
+
+import com.google.common.collect.ImmutableList;
+
+public class MockSubscription implements SubscriptionBase {
+
+ private final UUID id;
+ private final UUID bundleId;
+ private final EntitlementState state;
+ private Plan plan;
+ private final PlanPhase phase;
+ private final DateTime startDate;
+ private final List<EffectiveSubscriptionInternalEvent> transitions;
+
+ public MockSubscription(final UUID id, final UUID bundleId, final Plan plan, final DateTime startDate, final List<EffectiveSubscriptionInternalEvent> transitions) {
+ this.id = id;
+ this.bundleId = bundleId;
+ this.state = EntitlementState.ACTIVE;
+ this.plan = plan;
+ this.phase = null;
+ this.startDate = startDate;
+ this.transitions = transitions;
+ }
+
+ public MockSubscription(final EntitlementState state, final Plan plan, final PlanPhase phase) {
+ this.id = UUID.randomUUID();
+ this.bundleId = UUID.randomUUID();
+ this.state = state;
+ this.plan = plan;
+ this.phase = phase;
+ this.startDate = new DateTime(DateTimeZone.UTC);
+ this.transitions = ImmutableList.<EffectiveSubscriptionInternalEvent>of();
+ }
+
+ SubscriptionBase sub = Mockito.mock(SubscriptionBase.class);
+
+ @Override
+ public boolean cancel(final CallContext context) throws SubscriptionBaseApiException {
+ return sub.cancel(context);
+ }
+
+ @Override
+ public boolean cancelWithDate(final DateTime requestedDate, final CallContext context) throws SubscriptionBaseApiException {
+ return sub.cancelWithDate(requestedDate, context);
+ }
+
+ @Override
+ public boolean cancelWithPolicy(BillingActionPolicy policy, CallContext context)
+ throws SubscriptionBaseApiException {
+ return sub.cancelWithPolicy(policy, context);
+ }
+
+ @Override
+ public boolean uncancel(final CallContext context) throws SubscriptionBaseApiException {
+ return sub.uncancel(context);
+ }
+
+ @Override
+ public DateTime changePlan(final String productName, final BillingPeriod term, final String priceList, final CallContext context) throws SubscriptionBaseApiException {
+ return sub.changePlan(productName, term, priceList, context);
+ }
+
+ @Override
+ public DateTime changePlanWithDate(final String productName, final BillingPeriod term, final String priceList, final DateTime requestedDate,
+ final CallContext context) throws SubscriptionBaseApiException {
+ return sub.changePlanWithDate(productName, term, priceList, requestedDate, context);
+ }
+
+ @Override
+ public DateTime changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+ final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+ return sub.changePlanWithPolicy(productName, term, priceList, policy, context);
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public UUID getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public EntitlementState getState() {
+ return state;
+ }
+
+ @Override
+ public DateTime getStartDate() {
+ return startDate;
+ }
+
+ @Override
+ public DateTime getEndDate() {
+ return sub.getEndDate();
+ }
+
+ @Override
+ public Plan getCurrentPlan() {
+ return plan;
+ }
+
+ @Override
+ public PriceList getCurrentPriceList() {
+ return new MockPriceList();
+ }
+
+ @Override
+ public PlanPhase getCurrentPhase() {
+ return phase;
+ }
+
+ @Override
+ public DateTime getChargedThroughDate() {
+ return sub.getChargedThroughDate();
+ }
+
+ @Override
+ public ProductCategory getCategory() {
+ return sub.getCategory();
+ }
+
+ @Override
+ public DateTime getFutureEndDate() {
+ return sub.getFutureEndDate();
+ }
+
+ @Override
+ public EntitlementSourceType getSourceType() {
+ return sub.getSourceType();
+ }
+
+ @Override
+ public Product getLastActiveProduct() {
+ return sub.getLastActiveProduct();
+ }
+
+ @Override
+ public PriceList getLastActivePriceList() {
+ return sub.getLastActivePriceList();
+ }
+
+ @Override
+ public ProductCategory getLastActiveCategory() {
+ return sub.getLastActiveCategory();
+ }
+
+ @Override
+ public BillingPeriod getLastActiveBillingPeriod() {
+ return null;
+ }
+
+ @Override
+ public Plan getLastActivePlan() {
+ return sub.getLastActivePlan();
+ }
+
+ @Override
+ public PlanPhase getLastActivePhase() {
+ return sub.getLastActivePhase();
+ }
+
+ public void setPlan(final Plan plan) {
+ this.plan = plan;
+ }
+
+ @Override
+ public SubscriptionBaseTransition getPendingTransition() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public SubscriptionBaseTransition getPreviousTransition() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public List<SubscriptionBaseTransition> getAllTransitions() {
+ return null;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/payment/api/TestPaymentMethodPluginBase.java b/util/src/test/java/org/killbill/billing/payment/api/TestPaymentMethodPluginBase.java
new file mode 100644
index 0000000..8e96363
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/payment/api/TestPaymentMethodPluginBase.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestPaymentMethodPluginBase implements PaymentMethodPlugin {
+
+ @Override
+ public UUID getKbPaymentMethodId() {
+ return UUID.randomUUID();
+ }
+
+ @Override
+ public String getExternalPaymentMethodId() {
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ public boolean isDefaultPaymentMethod() {
+ return false;
+ }
+
+ @Override
+ public String getType() {
+ return "CreditCard";
+ }
+
+ @Override
+ public String getCCName() {
+ return "Bozo";
+ }
+
+ @Override
+ public String getCCType() {
+ return "Visa";
+ }
+
+ @Override
+ public String getCCExpirationMonth() {
+ return "12";
+ }
+
+ @Override
+ public String getCCExpirationYear() {
+ return "2013";
+ }
+
+ @Override
+ public String getCCLast4() {
+ return "4365";
+ }
+
+ @Override
+ public String getAddress1() {
+ return "34, street Foo";
+ }
+
+ @Override
+ public String getAddress2() {
+ return null;
+ }
+
+ @Override
+ public String getCity() {
+ return "SF";
+ }
+
+ @Override
+ public String getState() {
+ return "CA";
+ }
+
+ @Override
+ public String getZip() {
+ return "95321";
+ }
+
+ @Override
+ public String getCountry() {
+ return "Zimbawe";
+ }
+
+ @Override
+ public List<PaymentMethodKVInfo> getProperties() {
+ return ImmutableList.<PaymentMethodKVInfo>of();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/payment/plugin/api/PaymentPluginApiWithTestControl.java b/util/src/test/java/org/killbill/billing/payment/plugin/api/PaymentPluginApiWithTestControl.java
new file mode 100644
index 0000000..75f63c8
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/payment/plugin/api/PaymentPluginApiWithTestControl.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.plugin.api;
+
+public interface PaymentPluginApiWithTestControl extends PaymentPluginApi {
+
+ public void setPaymentPluginApiExceptionOnNextCalls(PaymentPluginApiException e);
+
+ public void setPaymentRuntimeExceptionOnNextCalls(RuntimeException e);
+
+ public void resetToNormalbehavior();
+}
diff --git a/util/src/test/java/org/killbill/billing/util/audit/api/TestDefaultAuditUserApi.java b/util/src/test/java/org/killbill/billing/util/audit/api/TestDefaultAuditUserApi.java
new file mode 100644
index 0000000..66129af
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/audit/api/TestDefaultAuditUserApi.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.api.AuditLevel;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.audit.AuditLogsTestBase;
+import org.killbill.billing.util.audit.dao.MockAuditDao;
+import org.killbill.billing.util.dao.TableName;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultAuditUserApi extends AuditLogsTestBase {
+
+ private List<AuditLog> auditLogs;
+ private List<UUID> objectIds;
+
+ @Override
+ @BeforeClass(groups = "fast")
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ auditLogs = ImmutableList.<AuditLog>of(createAuditLog(), createAuditLog(), createAuditLog(), createAuditLog());
+ objectIds = ImmutableList.<UUID>of(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID());
+
+ for (final TableName tableName : TableName.values()) {
+ for (final UUID objectId : objectIds) {
+ for (final AuditLog auditLog : auditLogs) {
+ ((MockAuditDao) auditDao).addAuditLogForId(tableName, objectId, auditLog);
+ }
+ }
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testForObject() throws Exception {
+ for (final ObjectType objectType : ObjectType.values()) {
+ for (final UUID objectId : objectIds) {
+ for (final AuditLevel level : AuditLevel.values()) {
+ if (AuditLevel.NONE.equals(level)) {
+ Assert.assertEquals(auditUserApi.getAuditLogs(objectId, objectType, level, callContext).size(), 0);
+ } else if (AuditLevel.MINIMAL.equals(level)) {
+ Assert.assertEquals(auditUserApi.getAuditLogs(objectId, objectType, level, callContext), ImmutableList.<AuditLog>of(auditLogs.get(0)));
+ } else {
+ Assert.assertEquals(auditUserApi.getAuditLogs(objectId, objectType, level, callContext), auditLogs);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/audit/AuditLogsTestBase.java b/util/src/test/java/org/killbill/billing/util/audit/AuditLogsTestBase.java
new file mode 100644
index 0000000..1025bac
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/audit/AuditLogsTestBase.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.mockito.Mockito;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public abstract class AuditLogsTestBase extends UtilTestSuiteNoDB {
+
+ protected ImmutableMap<UUID, List<AuditLog>> createAuditLogsAssociation() {
+ final UUID id1 = UUID.randomUUID();
+ final UUID id2 = UUID.randomUUID();
+ final UUID id3 = UUID.randomUUID();
+ return ImmutableMap.<UUID, List<AuditLog>>of(id1, ImmutableList.<AuditLog>of(createAuditLog(), createAuditLog()),
+ id2, ImmutableList.<AuditLog>of(createAuditLog(), createAuditLog()),
+ id3, ImmutableList.<AuditLog>of(createAuditLog(), createAuditLog()));
+ }
+
+ protected AuditLog createAuditLog() {
+ final AuditLog auditLog = Mockito.mock(AuditLog.class);
+ Mockito.when(auditLog.getCreatedDate()).thenReturn(clock.getUTCNow());
+ Mockito.when(auditLog.getReasonCode()).thenReturn(UUID.randomUUID().toString());
+ Mockito.when(auditLog.getUserName()).thenReturn(UUID.randomUUID().toString());
+ Mockito.when(auditLog.getUserToken()).thenReturn(UUID.randomUUID().toString());
+ Mockito.when(auditLog.getComment()).thenReturn(UUID.randomUUID().toString());
+ Mockito.when(auditLog.getChangeType()).thenReturn(ChangeType.DELETE);
+
+ return auditLog;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/audit/dao/MockAuditDao.java b/util/src/test/java/org/killbill/billing/util/audit/dao/MockAuditDao.java
new file mode 100644
index 0000000..f1cb189
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/audit/dao/MockAuditDao.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit.dao;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.api.AuditLevel;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.audit.DefaultAccountAuditLogs;
+import org.killbill.billing.util.audit.DefaultAccountAuditLogsForObjectType;
+import org.killbill.billing.util.dao.TableName;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+
+public class MockAuditDao implements AuditDao {
+
+ private final Map<TableName, Map<UUID, List<AuditLog>>> auditLogsForTables = new HashMap<TableName, Map<UUID, List<AuditLog>>>();
+
+ public synchronized void addAuditLogForId(final TableName tableName, final UUID objectId, final AuditLog auditLog) {
+ addAuditLogsForId(tableName, objectId, ImmutableList.<AuditLog>of(auditLog));
+ }
+
+ public synchronized void addAuditLogsForId(final TableName tableName, final UUID objectId, final List<AuditLog> auditLogs) {
+ if (auditLogsForTables.get(tableName) == null) {
+ auditLogsForTables.put(tableName, new HashMap<UUID, List<AuditLog>>());
+ }
+
+ if (auditLogsForTables.get(tableName).get(objectId) == null) {
+ auditLogsForTables.get(tableName).put(objectId, new ArrayList<AuditLog>());
+ }
+
+ auditLogsForTables.get(tableName).get(objectId).addAll(auditLogs);
+ }
+
+ @Override
+ public DefaultAccountAuditLogs getAuditLogsForAccountRecordId(final AuditLevel auditLevel, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DefaultAccountAuditLogsForObjectType getAuditLogsForAccountRecordId(final TableName tableName, final AuditLevel auditLevel, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForId(final TableName tableName, final UUID objectId, final AuditLevel auditLevel, final InternalTenantContext context) {
+ final Map<UUID, List<AuditLog>> auditLogsForTableName = auditLogsForTables.get(tableName);
+ if (auditLogsForTableName == null) {
+ return ImmutableList.<AuditLog>of();
+ }
+
+ final List<AuditLog> auditLogsForObjectId = auditLogsForTableName.get(objectId);
+ final List<AuditLog> allAuditLogs = Objects.firstNonNull(auditLogsForObjectId, ImmutableList.<AuditLog>of());
+ if (AuditLevel.FULL.equals(auditLevel)) {
+ return allAuditLogs;
+ } else if (AuditLevel.MINIMAL.equals(auditLevel) && allAuditLogs.size() > 0) {
+ return ImmutableList.<AuditLog>of(allAuditLogs.get(0));
+ } else if (AuditLevel.NONE.equals(auditLevel)) {
+ return ImmutableList.<AuditLog>of();
+ } else {
+ return allAuditLogs;
+ }
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/audit/dao/TestDefaultAuditDao.java b/util/src/test/java/org/killbill/billing/util/audit/dao/TestDefaultAuditDao.java
new file mode 100644
index 0000000..4249b18
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/audit/dao/TestDefaultAuditDao.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.Handle;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.api.AuditLevel;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.audit.AccountAuditLogs;
+import org.killbill.billing.util.audit.AccountAuditLogsForObjectType;
+import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.dao.TableName;
+import org.killbill.billing.util.tag.DescriptiveTag;
+import org.killbill.billing.util.tag.Tag;
+import org.killbill.billing.util.tag.dao.TagDefinitionModelDao;
+import org.killbill.billing.util.tag.dao.TagModelDao;
+
+public class TestDefaultAuditDao extends UtilTestSuiteWithEmbeddedDB {
+
+ private TagModelDao tag;
+
+ @Test(groups = "slow")
+ public void testRetrieveAuditsDirectly() throws Exception {
+ addTag();
+
+ // Verify we get an audit entry for the tag_history table
+ final Handle handle = dbi.open();
+ final String tagHistoryString = (String) handle.select("select id from tag_history limit 1").get(0).get("id");
+ handle.close();
+
+ for (final AuditLevel level : AuditLevel.values()) {
+ final List<AuditLog> auditLogs = auditDao.getAuditLogsForId(TableName.TAG_HISTORY, UUID.fromString(tagHistoryString), level, internalCallContext);
+ verifyAuditLogsForTag(auditLogs, level);
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testRetrieveAuditsViaHistory() throws Exception {
+ addTag();
+
+ for (final AuditLevel level : AuditLevel.values()) {
+ final List<AuditLog> auditLogs = auditDao.getAuditLogsForId(TableName.TAG, tag.getId(), level, internalCallContext);
+ verifyAuditLogsForTag(auditLogs, level);
+
+ final AccountAuditLogs accountAuditLogs = auditDao.getAuditLogsForAccountRecordId(level, internalCallContext);
+ verifyAuditLogsForTag(accountAuditLogs.getAuditLogs(ObjectType.TAG).getAuditLogs(tag.getId()), level);
+
+ final AccountAuditLogsForObjectType accountAuditLogsForObjectType = auditDao.getAuditLogsForAccountRecordId(TableName.TAG, level, internalCallContext);
+ verifyAuditLogsForTag(accountAuditLogsForObjectType.getAuditLogs(tag.getId()), level);
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testVerifyAuditCachesAreCleared() throws Exception {
+ addTag();
+ final List<AuditLog> firstAuditLogs = auditDao.getAuditLogsForId(TableName.TAG, tag.getId(), AuditLevel.FULL, internalCallContext);
+ Assert.assertEquals(firstAuditLogs.size(), 1);
+ Assert.assertEquals(firstAuditLogs.get(0).getChangeType(), ChangeType.INSERT);
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG);
+ tagDao.deleteTag(tag.getObjectId(), tag.getObjectType(), tag.getTagDefinitionId(), internalCallContext);
+ assertListenerStatus();
+
+ final List<AuditLog> secondAuditLogs = auditDao.getAuditLogsForId(TableName.TAG, tag.getId(), AuditLevel.FULL, internalCallContext);
+ Assert.assertEquals(secondAuditLogs.size(), 2);
+ Assert.assertEquals(secondAuditLogs.get(0).getChangeType(), ChangeType.INSERT);
+ Assert.assertEquals(secondAuditLogs.get(1).getChangeType(), ChangeType.DELETE);
+ }
+
+ private void addTag() throws TagDefinitionApiException, TagApiException {
+ // Create a tag definition
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ final TagDefinitionModelDao tagDefinition = tagDefinitionDao.create(UUID.randomUUID().toString().substring(0, 5),
+ UUID.randomUUID().toString().substring(0, 5),
+ internalCallContext);
+ assertListenerStatus();
+
+ Assert.assertEquals(tagDefinitionDao.getById(tagDefinition.getId(), internalCallContext), tagDefinition);
+
+ // Create a tag
+ final UUID objectId = UUID.randomUUID();
+
+ final Tag theTag = new DescriptiveTag(tagDefinition.getId(), ObjectType.ACCOUNT, objectId, clock.getUTCNow());
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG);
+ tagDao.create(new TagModelDao(theTag), internalCallContext);
+ assertListenerStatus();
+
+ final List<TagModelDao> tags = tagDao.getTagsForObject(objectId, ObjectType.ACCOUNT, false, internalCallContext);
+ Assert.assertEquals(tags.size(), 1);
+ tag = tags.get(0);
+ Assert.assertEquals(tag.getTagDefinitionId(), tagDefinition.getId());
+ }
+
+ private void verifyAuditLogsForTag(final List<AuditLog> auditLogs, final AuditLevel level) {
+ if (AuditLevel.NONE.equals(level)) {
+ Assert.assertEquals(auditLogs.size(), 0);
+ return;
+ }
+
+ Assert.assertEquals(auditLogs.size(), 1);
+ Assert.assertEquals(auditLogs.get(0).getUserToken(), internalCallContext.getUserToken().toString());
+ Assert.assertEquals(auditLogs.get(0).getChangeType(), ChangeType.INSERT);
+ Assert.assertEquals(auditLogs.get(0).getComment(), internalCallContext.getComments());
+ Assert.assertEquals(auditLogs.get(0).getReasonCode(), internalCallContext.getReasonCode());
+ Assert.assertEquals(auditLogs.get(0).getUserName(), internalCallContext.getCreatedBy());
+ Assert.assertNotNull(auditLogs.get(0).getCreatedDate());
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/audit/TestDefaultAuditLog.java b/util/src/test/java/org/killbill/billing/util/audit/TestDefaultAuditLog.java
new file mode 100644
index 0000000..7de9941
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/audit/TestDefaultAuditLog.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.audit;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.DefaultCallContext;
+import org.killbill.clock.ClockMock;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.audit.dao.AuditLogModelDao;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.CallOrigin;
+import org.killbill.billing.util.callcontext.UserType;
+import org.killbill.billing.util.dao.EntityAudit;
+import org.killbill.billing.util.dao.TableName;
+
+public class TestDefaultAuditLog extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testGetters() throws Exception {
+ final TableName tableName = TableName.ACCOUNT_EMAIL_HISTORY;
+ final long recordId = Long.MAX_VALUE;
+ final ChangeType changeType = ChangeType.DELETE;
+ final EntityAudit entityAudit = new EntityAudit(tableName, recordId, changeType, null);
+
+ final UUID tenantId = UUID.randomUUID();
+ final String userName = UUID.randomUUID().toString();
+ final CallOrigin callOrigin = CallOrigin.EXTERNAL;
+ final UserType userType = UserType.CUSTOMER;
+ final UUID userToken = UUID.randomUUID();
+ final ClockMock clock = new ClockMock();
+ final CallContext callContext = new DefaultCallContext(tenantId, userName, callOrigin, userType, userToken, clock);
+
+ final AuditLog auditLog = new DefaultAuditLog(new AuditLogModelDao(entityAudit, callContext), ObjectType.ACCOUNT_EMAIL, UUID.randomUUID());
+ Assert.assertEquals(auditLog.getChangeType(), changeType);
+ Assert.assertNull(auditLog.getComment());
+ Assert.assertNotNull(auditLog.getCreatedDate());
+ Assert.assertNull(auditLog.getReasonCode());
+ Assert.assertEquals(auditLog.getUserName(), userName);
+ Assert.assertEquals(auditLog.getUserToken(), userToken.toString());
+ }
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final TableName tableName = TableName.ACCOUNT_EMAIL_HISTORY;
+ final long recordId = Long.MAX_VALUE;
+ final ChangeType changeType = ChangeType.DELETE;
+ final EntityAudit entityAudit = new EntityAudit(tableName, recordId, changeType, null);
+
+ final UUID tenantId = UUID.randomUUID();
+ final String userName = UUID.randomUUID().toString();
+ final CallOrigin callOrigin = CallOrigin.EXTERNAL;
+ final UserType userType = UserType.CUSTOMER;
+ final UUID userToken = UUID.randomUUID();
+ final ClockMock clock = new ClockMock();
+ final CallContext callContext = new DefaultCallContext(tenantId, userName, callOrigin, userType, userToken, clock);
+
+ final AuditLogModelDao auditLog = new AuditLogModelDao(entityAudit, callContext);
+ Assert.assertEquals(auditLog, auditLog);
+
+ final AuditLogModelDao sameAuditLog = new AuditLogModelDao(entityAudit, callContext);
+ Assert.assertEquals(sameAuditLog, auditLog);
+
+ clock.addMonths(1);
+ final CallContext otherCallContext = new DefaultCallContext(tenantId, userName, callOrigin, userType, userToken, clock);
+ final AuditLogModelDao otherAuditLog = new AuditLogModelDao(entityAudit, otherCallContext);
+ Assert.assertNotEquals(otherAuditLog, auditLog);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/cache/TestCache.java b/util/src/test/java/org/killbill/billing/util/cache/TestCache.java
new file mode 100644
index 0000000..0a797a9
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/cache/TestCache.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.cache;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
+import org.killbill.billing.util.tag.dao.TagModelDao;
+import org.killbill.billing.util.tag.dao.TagSqlDao;
+
+public class TestCache extends UtilTestSuiteWithEmbeddedDB {
+
+ private EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
+
+ private void insertTag(final TagModelDao modelDao) {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ entitySqlDaoWrapperFactory.become(TagSqlDao.class).create(modelDao, internalCallContext);
+ return null;
+ }
+ });
+ }
+
+ private Long getTagRecordId(final UUID tagId) {
+ return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Long>() {
+ @Override
+ public Long inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ return entitySqlDaoWrapperFactory.become(TagSqlDao.class).getRecordId(tagId.toString(), internalCallContext);
+ }
+ });
+ }
+
+ private int getCacheSize() {
+ final CacheController<Object, Object> cache = controlCacheDispatcher.getCacheController(CacheType.RECORD_ID);
+ return cache != null ? cache.size() : 0;
+ }
+
+ private Long retrieveRecordIdFromCache(final UUID tagId) {
+ final CacheController<Object, Object> cache = controlCacheDispatcher.getCacheController(CacheType.RECORD_ID);
+ Object result = null;
+ if (cache != null) {
+ // Keys are upper cased by convention
+ result = cache.get(tagId.toString().toUpperCase(), new CacheLoaderArgument(ObjectType.TAG));
+ }
+ return (Long) result;
+ }
+
+ @Test(groups = "slow")
+ public void testCacheRecordId() throws Exception {
+ this.transactionalSqlDao = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, controlCacheDispatcher, nonEntityDao);
+ final TagModelDao tag = new TagModelDao(clock.getUTCNow(), UUID.randomUUID(), UUID.randomUUID(), ObjectType.TAG);
+
+ // Verify we start with nothing in the cache
+ Assert.assertEquals(getCacheSize(), 0);
+ insertTag(tag);
+
+ // Verify we still have nothing after insert in the cache
+ Assert.assertEquals(getCacheSize(), 0);
+
+ final Long tagRecordId = getTagRecordId(tag.getId());
+ // Verify we now have something in the cache
+ Assert.assertEquals(getCacheSize(), 1);
+
+ final Long recordIdFromCache = retrieveRecordIdFromCache(tag.getId());
+ Assert.assertNotNull(recordIdFromCache);
+
+ Assert.assertEquals(recordIdFromCache, new Long(1));
+ Assert.assertEquals(tagRecordId, new Long(1));
+
+ Assert.assertEquals(getCacheSize(), 1);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/callcontext/TestCallContext.java b/util/src/test/java/org/killbill/billing/util/callcontext/TestCallContext.java
new file mode 100644
index 0000000..5798beb
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/callcontext/TestCallContext.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.callcontext;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import org.killbill.clock.DefaultClock;
+
+public class TestCallContext implements CallContext {
+
+ private final String userName;
+ private final DateTime updatedDate;
+ private final DateTime createdDate;
+ private final UUID userToken;
+ private final UUID tenantId;
+
+ public TestCallContext(final String userName) {
+ this(userName, new DefaultClock().getUTCNow(), new DefaultClock().getUTCNow());
+ }
+
+ public TestCallContext(final String userName, final DateTime createdDate, final DateTime updatedDate) {
+ this.userName = userName;
+ this.createdDate = createdDate;
+ this.updatedDate = updatedDate;
+ this.userToken = UUID.randomUUID();
+ this.tenantId = UUID.randomUUID();
+ }
+
+ @Override
+ public String getUserName() {
+ return userName;
+ }
+
+ @Override
+ public CallOrigin getCallOrigin() {
+ return CallOrigin.TEST;
+ }
+
+ @Override
+ public UserType getUserType() {
+ return UserType.TEST;
+ }
+
+ @Override
+ public String getReasonCode() {
+ return null;
+ }
+
+ @Override
+ public String getComments() {
+ return null;
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return createdDate;
+ }
+
+ @Override
+ public DateTime getUpdatedDate() {
+ return updatedDate;
+ }
+
+ @Override
+ public UUID getUserToken() {
+ return userToken;
+ }
+
+ @Override
+ public UUID getTenantId() {
+ return tenantId;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/callcontext/TestDefaultCallContext.java b/util/src/test/java/org/killbill/billing/util/callcontext/TestDefaultCallContext.java
new file mode 100644
index 0000000..376f8f5
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/callcontext/TestDefaultCallContext.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.callcontext;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.callcontext.DefaultCallContext;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+
+public class TestDefaultCallContext extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testGetters() throws Exception {
+ final UUID tenantId = UUID.randomUUID();
+ final String userName = UUID.randomUUID().toString();
+ final DateTime createdDate = clock.getUTCNow();
+ final String reasonCode = UUID.randomUUID().toString();
+ final String comment = UUID.randomUUID().toString();
+ final UUID userToken = UUID.randomUUID();
+ final DefaultCallContext callContext = new DefaultCallContext(tenantId, userName, createdDate, reasonCode, comment, userToken);
+
+ Assert.assertEquals(callContext.getTenantId(), tenantId);
+ Assert.assertEquals(callContext.getCreatedDate(), createdDate);
+ Assert.assertNull(callContext.getCallOrigin());
+ Assert.assertEquals(callContext.getComments(), comment);
+ Assert.assertEquals(callContext.getReasonCode(), reasonCode);
+ Assert.assertEquals(callContext.getUserName(), userName);
+ Assert.assertEquals(callContext.getUpdatedDate(), createdDate);
+ Assert.assertEquals(callContext.getUserToken(), userToken);
+ Assert.assertNull(callContext.getUserType());
+ }
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final UUID tenantId = UUID.randomUUID();
+ final String userName = UUID.randomUUID().toString();
+ final DateTime createdDate = clock.getUTCNow();
+ final String reasonCode = UUID.randomUUID().toString();
+ final String comment = UUID.randomUUID().toString();
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultCallContext callContext = new DefaultCallContext(tenantId, userName, createdDate, reasonCode, comment, userToken);
+ Assert.assertEquals(callContext, callContext);
+
+ final DefaultCallContext sameCallContext = new DefaultCallContext(tenantId, userName, createdDate, reasonCode, comment, userToken);
+ Assert.assertEquals(sameCallContext, callContext);
+
+ final DefaultCallContext otherCallContext = new DefaultCallContext(tenantId, UUID.randomUUID().toString(), createdDate, reasonCode, comment, userToken);
+ Assert.assertNotEquals(otherCallContext, callContext);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/callcontext/TestInternalCallContextFactory.java b/util/src/test/java/org/killbill/billing/util/callcontext/TestInternalCallContextFactory.java
new file mode 100644
index 0000000..3b863a5
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/callcontext/TestInternalCallContextFactory.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.callcontext;
+
+import java.util.Date;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+
+public class TestInternalCallContextFactory extends UtilTestSuiteWithEmbeddedDB {
+
+
+ @Test(groups = "slow")
+ public void testCreateInternalCallContextWithAccountRecordIdFromSimpleObjectType() throws Exception {
+ final UUID invoiceId = UUID.randomUUID();
+ final Long accountRecordId = 19384012L;
+
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ handle.execute("DROP TABLE IF EXISTS invoices;\n" +
+ "CREATE TABLE invoices (\n" +
+ " record_id int(11) unsigned NOT NULL AUTO_INCREMENT,\n" +
+ " id char(36) NOT NULL,\n" +
+ " account_id char(36) NOT NULL,\n" +
+ " invoice_date date NOT NULL,\n" +
+ " target_date date NOT NULL,\n" +
+ " currency char(3) NOT NULL,\n" +
+ " migrated bool NOT NULL,\n" +
+ " created_by varchar(50) NOT NULL,\n" +
+ " created_date datetime NOT NULL,\n" +
+ " account_record_id int(11) unsigned default null,\n" +
+ " tenant_record_id int(11) unsigned default null,\n" +
+ " PRIMARY KEY(record_id)\n" +
+ ");");
+ handle.execute("insert into invoices (id, account_id, invoice_date, target_date, currency, migrated, created_by, created_date, account_record_id) values " +
+ "(?, ?, now(), now(), 'USD', 0, 'test', now(), ?)", invoiceId.toString(), UUID.randomUUID().toString(), accountRecordId);
+ return null;
+ }
+ });
+
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(invoiceId, ObjectType.INVOICE, callContext);
+ // The account record id should have been looked up in the invoices table
+ Assert.assertEquals(context.getAccountRecordId(), accountRecordId);
+ verifyInternalCallContext(context);
+ }
+
+ @Test(groups = "slow")
+ public void testCreateInternalCallContextWithAccountRecordIdFromAccountObjectType() throws Exception {
+ final UUID accountId = UUID.randomUUID();
+ final Long accountRecordId = 19384012L;
+
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ // Note: we always create an accounts table, see MysqlTestingHelper
+ handle.execute("insert into accounts (record_id, id, email, name, first_name_length, is_notified_for_invoices, created_date, created_by, updated_date, updated_by) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ accountRecordId, accountId.toString(), "yo@t.com", "toto", 4, false, new Date(), "i", new Date(), "j");
+ return null;
+ }
+ });
+
+ final InternalCallContext context = internalCallContextFactory.createInternalCallContext(accountId, ObjectType.ACCOUNT, callContext);
+ // The account record id should have been looked up in the accounts table
+ Assert.assertEquals(context.getAccountRecordId(), accountRecordId);
+ verifyInternalCallContext(context);
+ }
+
+ private void verifyInternalCallContext(final InternalCallContext context) {
+ Assert.assertEquals(context.getCallOrigin(), callContext.getCallOrigin());
+ Assert.assertEquals(context.getComments(), callContext.getComments());
+ Assert.assertEquals(context.getCreatedDate(), callContext.getCreatedDate());
+ Assert.assertEquals(context.getReasonCode(), callContext.getReasonCode());
+ Assert.assertEquals(context.getUpdatedDate(), callContext.getUpdatedDate());
+ Assert.assertEquals(context.getCreatedBy(), callContext.getUserName());
+ Assert.assertEquals(context.getUserToken(), callContext.getUserToken());
+ Assert.assertEquals(context.getContextUserType(), callContext.getUserType());
+ // Our test callcontext doesn't have a tenant id
+ Assert.assertEquals(context.getTenantRecordId(), (Long) InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/config/TestXMLLoader.java b/util/src/test/java/org/killbill/billing/util/config/TestXMLLoader.java
new file mode 100644
index 0000000..0e0166d
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/config/TestXMLLoader.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import javax.xml.bind.JAXBException;
+import javax.xml.transform.TransformerException;
+
+import org.testng.annotations.Test;
+import org.xml.sax.SAXException;
+
+import org.killbill.billing.catalog.api.InvalidConfigException;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.config.catalog.ValidationException;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+
+import static org.testng.Assert.assertEquals;
+
+
+public class TestXMLLoader extends UtilTestSuiteNoDB {
+ public static final String TEST_XML =
+ "<xmlTestClass>" +
+ " <foo>foo</foo>" +
+ " <bar>1.0</bar>" +
+ " <lala>42</lala>" +
+ "</xmlTestClass>";
+
+ @Test(groups = "fast")
+ public void test() throws SAXException, InvalidConfigException, JAXBException, IOException, TransformerException, URISyntaxException, ValidationException {
+ final InputStream is = new ByteArrayInputStream(TEST_XML.getBytes());
+ final XmlTestClass test = XMLLoader.getObjectFromStream(new URI("internal:/"), is, XmlTestClass.class);
+ assertEquals(test.getFoo(), "foo");
+ assertEquals(test.getBar(), 1.0);
+ assertEquals(test.getLala(), 42);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/config/TestXMLSchemaGenerator.java b/util/src/test/java/org/killbill/billing/util/config/TestXMLSchemaGenerator.java
new file mode 100644
index 0000000..530dc6c
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/config/TestXMLSchemaGenerator.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.xml.bind.JAXBException;
+import javax.xml.transform.TransformerException;
+
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.config.catalog.XMLSchemaGenerator;
+import org.killbill.billing.util.io.IOUtils;
+
+public class TestXMLSchemaGenerator extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast", enabled = false)
+ public void test() throws IOException, TransformerException, JAXBException {
+ final InputStream stream = XMLSchemaGenerator.xmlSchema(XmlTestClass.class);
+ System.out.println(IOUtils.toString(stream));
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/config/TestXMLWriter.java b/util/src/test/java/org/killbill/billing/util/config/TestXMLWriter.java
new file mode 100644
index 0000000..6c560b3
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/config/TestXMLWriter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
+
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.config.catalog.XMLLoader;
+import org.killbill.billing.util.config.catalog.XMLWriter;
+
+import static org.testng.Assert.assertEquals;
+
+public class TestXMLWriter extends UtilTestSuiteNoDB {
+
+ public static final String TEST_XML =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
+ "<xmlTestClass>" +
+ "<foo>foo</foo>" +
+ "<bar>1.0</bar>" +
+ "<lala>42</lala>" +
+ "</xmlTestClass>";
+
+ @Test(groups = "fast")
+ public void test() throws Exception {
+ final InputStream is = new ByteArrayInputStream(TEST_XML.getBytes());
+ final XmlTestClass test = XMLLoader.getObjectFromStream(new URI("internal:/"), is, XmlTestClass.class);
+ assertEquals(test.getFoo(), "foo");
+ assertEquals(test.getBar(), 1.0);
+ assertEquals(test.getLala(), 42);
+
+ final String output = XMLWriter.writeXML(test, XmlTestClass.class);
+ //System.out.println(output);
+ assertEquals(output.replaceAll("\\s", ""), TEST_XML.replaceAll("\\s", ""));
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/config/XmlTestClass.java b/util/src/test/java/org/killbill/billing/util/config/XmlTestClass.java
new file mode 100644
index 0000000..6a1d3db
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/config/XmlTestClass.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.config;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.killbill.billing.util.config.catalog.ValidatingConfig;
+import org.killbill.billing.util.config.catalog.ValidationErrors;
+
+@XmlRootElement
+@XmlAccessorType(XmlAccessType.FIELD)
+public class XmlTestClass extends ValidatingConfig<XmlTestClass> {
+ private String foo;
+ private Double bar;
+ private int lala;
+
+ public String getFoo() {
+ return foo;
+ }
+
+ public Double getBar() {
+ return bar;
+ }
+
+ public int getLala() {
+ return lala;
+ }
+
+ @Override
+ public ValidationErrors validate(final XmlTestClass root, final ValidationErrors errors) {
+ return errors;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldCreationEvent.java b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldCreationEvent.java
new file mode 100644
index 0000000..f088f46
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldCreationEvent.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.api;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.events.BusInternalEvent.BusInternalEventType;
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+public class TestDefaultCustomFieldCreationEvent {
+
+
+ @Test(groups = "fast")
+ public void testPojo() throws Exception {
+ final UUID customFieldId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+
+ final DefaultCustomFieldCreationEvent event = new DefaultCustomFieldCreationEvent(customFieldId, objectId, objectType, 1L, 2L, UUID.randomUUID());
+ Assert.assertEquals(event.getBusEventType(), BusInternalEventType.CUSTOM_FIELD_CREATION);
+
+ Assert.assertEquals(event.getObjectId(), objectId);
+ Assert.assertEquals(event.getObjectType(), objectType);
+
+ Assert.assertEquals(event, event);
+ Assert.assertEquals(event, new DefaultCustomFieldCreationEvent(customFieldId, objectId, objectType, 1L, 2L, UUID.randomUUID()));
+ }
+
+ @Test(groups = "fast")
+ public void testSerialization() throws Exception {
+
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final UUID customFieldId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+
+ final DefaultCustomFieldCreationEvent event = new DefaultCustomFieldCreationEvent(customFieldId, objectId, objectType, 1L, 2L, UUID.randomUUID());
+
+ final String json = objectMapper.writeValueAsString(event);
+ final DefaultCustomFieldCreationEvent fromJson = objectMapper.readValue(json, DefaultCustomFieldCreationEvent.class);
+ Assert.assertEquals(fromJson, event);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldDeletionEvent.java b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldDeletionEvent.java
new file mode 100644
index 0000000..413f251
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldDeletionEvent.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.api;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.events.BusInternalEvent.BusInternalEventType;
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+public class TestDefaultCustomFieldDeletionEvent {
+
+
+ @Test(groups = "fast")
+ public void testPojo() throws Exception {
+ final UUID customFieldId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultCustomFieldDeletionEvent event = new DefaultCustomFieldDeletionEvent(customFieldId, objectId, objectType, 1L, 2L, UUID.randomUUID());
+ Assert.assertEquals(event.getBusEventType(), BusInternalEventType.CUSTOM_FIELD_DELETION);
+
+ Assert.assertEquals(event.getObjectId(), objectId);
+ Assert.assertEquals(event.getObjectType(), objectType);
+
+ Assert.assertEquals(event, event);
+ Assert.assertEquals(event, new DefaultCustomFieldDeletionEvent(customFieldId, objectId, objectType, 1L, 2L, UUID.randomUUID()));
+ }
+
+ @Test(groups = "fast")
+ public void testSerialization() throws Exception {
+
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final UUID customFieldId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultCustomFieldDeletionEvent event = new DefaultCustomFieldDeletionEvent(customFieldId, objectId, objectType, 1L, 2L, UUID.randomUUID());
+
+ final String json = objectMapper.writeValueAsString(event);
+ final DefaultCustomFieldDeletionEvent fromJson = objectMapper.readValue(json, DefaultCustomFieldDeletionEvent.class);
+ Assert.assertEquals(fromJson, event);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java
new file mode 100644
index 0000000..3be0174
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.api;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.customfield.CustomField;
+import org.killbill.billing.util.customfield.StringCustomField;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultCustomFieldUserApi extends UtilTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testSaveCustomFieldWithAccountRecordId() throws Exception {
+ final UUID accountId = UUID.randomUUID();
+ final Long accountRecordId = 19384012L;
+
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ // Note: we always create an accounts table, see MysqlTestingHelper
+ handle.execute("insert into accounts (record_id, id, email, name, first_name_length, is_notified_for_invoices, created_date, created_by, updated_date, updated_by) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ accountRecordId, accountId.toString(), "yo@t.com", "toto", 4, false, new Date(), "i", new Date(), "j");
+
+ return null;
+ }
+ });
+
+ final String cfName = UUID.randomUUID().toString().substring(1, 4);
+ final String cfValue = UUID.randomUUID().toString().substring(1, 4);
+ final CustomField customField = new StringCustomField(cfName, cfValue, ObjectType.ACCOUNT, accountId, callContext.getCreatedDate());
+ eventsListener.pushExpectedEvent(NextEvent.CUSTOM_FIELD);
+ customFieldUserApi.addCustomFields(ImmutableList.<CustomField>of(customField), callContext);
+ assertListenerStatus();
+
+ // Verify the field was saved
+ final List<CustomField> customFields = customFieldUserApi.getCustomFieldsForObject(accountId, ObjectType.ACCOUNT, callContext);
+ Assert.assertEquals(customFields.size(), 1);
+ Assert.assertEquals(customFields.get(0), customField);
+ // Verify the account_record_id was populated
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ final List<Map<String, Object>> values = handle.select("select account_record_id from custom_fields where object_id = ?", accountId.toString());
+ Assert.assertEquals(values.size(), 1);
+ Assert.assertEquals(values.get(0).keySet().size(), 1);
+ Assert.assertEquals(Long.valueOf(values.get(0).get("account_record_id").toString()), accountRecordId);
+ return null;
+ }
+ });
+
+ customFieldUserApi.removeCustomFields(customFields, callContext);
+ List<CustomField> remainingCustomFields = customFieldUserApi.getCustomFieldsForObject(accountId, ObjectType.ACCOUNT, callContext);
+ Assert.assertEquals(remainingCustomFields.size(), 0);
+
+ // Add again the custom field
+ final CustomField newCustomField = new StringCustomField(cfName, cfValue, ObjectType.ACCOUNT, accountId, callContext.getCreatedDate());
+
+ eventsListener.pushExpectedEvent(NextEvent.CUSTOM_FIELD);
+ customFieldUserApi.addCustomFields(ImmutableList.<CustomField>of(newCustomField), callContext);
+ remainingCustomFields = customFieldUserApi.getCustomFieldsForObject(accountId, ObjectType.ACCOUNT, callContext);
+ Assert.assertEquals(remainingCustomFields.size(), 1);
+
+ // Delete again
+ customFieldUserApi.removeCustomFields(remainingCustomFields, callContext);
+ remainingCustomFields = customFieldUserApi.getCustomFieldsForObject(accountId, ObjectType.ACCOUNT, callContext);
+ Assert.assertEquals(remainingCustomFields.size(), 0);
+
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/customfield/dao/MockCustomFieldDao.java b/util/src/test/java/org/killbill/billing/util/customfield/dao/MockCustomFieldDao.java
new file mode 100644
index 0000000..e2b016d
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/customfield/dao/MockCustomFieldDao.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield.dao;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.customfield.CustomField;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.MockEntityDaoBase;
+
+public class MockCustomFieldDao extends MockEntityDaoBase<CustomFieldModelDao, CustomField, CustomFieldApiException> implements CustomFieldDao {
+
+ @Override
+ public List<CustomFieldModelDao> getCustomFieldsForObject(final UUID objectId, final ObjectType objectType, final InternalTenantContext context) {
+ final List<CustomFieldModelDao> result = new ArrayList<CustomFieldModelDao>();
+ final Iterable<CustomFieldModelDao> all = getAll(context);
+ for (final CustomFieldModelDao cur : all) {
+ if (cur.getObjectId().equals(objectId) && cur.getObjectType() == objectType) {
+ result.add(cur);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public List<CustomFieldModelDao> getCustomFieldsForAccountType(final ObjectType objectType, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<CustomFieldModelDao> getCustomFieldsForAccount(final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Pagination<CustomFieldModelDao> searchCustomFields(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void deleteCustomField(final UUID customFieldId, final InternalCallContext context) throws CustomFieldApiException {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/customfield/MockCustomFieldModuleMemory.java b/util/src/test/java/org/killbill/billing/util/customfield/MockCustomFieldModuleMemory.java
new file mode 100644
index 0000000..5a6ed0a
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/customfield/MockCustomFieldModuleMemory.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield;
+
+import org.killbill.billing.util.customfield.dao.CustomFieldDao;
+import org.killbill.billing.util.customfield.dao.MockCustomFieldDao;
+import org.killbill.billing.util.glue.CustomFieldModule;
+
+public class MockCustomFieldModuleMemory extends CustomFieldModule {
+ @Override
+ protected void installCustomFieldDao() {
+ bind(CustomFieldDao.class).to(MockCustomFieldDao.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/customfield/TestFieldStore.java b/util/src/test/java/org/killbill/billing/util/customfield/TestFieldStore.java
new file mode 100644
index 0000000..9051563
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/customfield/TestFieldStore.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.customfield;
+
+import java.util.UUID;
+
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.customfield.dao.CustomFieldModelDao;
+
+public class TestFieldStore extends UtilTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testCreateCustomField() throws CustomFieldApiException {
+ final UUID id = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT;
+
+ String fieldName = "TestField1";
+ String fieldValue = "Kitty Hawk";
+
+ final CustomField field = new StringCustomField(fieldName, fieldValue, objectType, id, internalCallContext.getCreatedDate());
+ eventsListener.pushExpectedEvent(NextEvent.CUSTOM_FIELD);
+ customFieldDao.create(new CustomFieldModelDao(field), internalCallContext);
+ assertListenerStatus();
+
+ fieldName = "TestField2";
+ fieldValue = "Cape Canaveral";
+ final CustomField field2 = new StringCustomField(fieldName, fieldValue, objectType, id, internalCallContext.getCreatedDate());
+ eventsListener.pushExpectedEvent(NextEvent.CUSTOM_FIELD);
+ customFieldDao.create(new CustomFieldModelDao(field2), internalCallContext);
+ assertListenerStatus();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/dao/TestNonEntityDao.java b/util/src/test/java/org/killbill/billing/util/dao/TestNonEntityDao.java
new file mode 100644
index 0000000..d41bebd
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/dao/TestNonEntityDao.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+
+public class TestNonEntityDao extends UtilTestSuiteWithEmbeddedDB {
+
+ final Long tenantRecordId = 123123123L;
+ final UUID tenantId = UUID.fromString("121c59d4-0458-4038-a683-698c9a121c12");
+
+ final UUID accountId = UUID.fromString("a01c59d4-0458-4038-a683-698c9a121c69");
+ final Long accountRecordId = 333333L;
+
+ final UUID tagDefinitionId = UUID.fromString("e01c59d4-0458-4038-a683-698c9a121c34");
+ final Long tagDefinitionRecordId = 44444444L;
+
+ final UUID tagId = UUID.fromString("123c59d4-0458-4038-a683-698c9a121456");
+ final Long tagRecordId = 55555555L;
+
+ @Test(groups = "slow")
+ public void testRetrieveRecordIdFromObject() throws IOException {
+ insertAccount();
+
+ final Long resultRecordId = nonEntityDao.retrieveRecordIdFromObject(accountId, ObjectType.ACCOUNT, null);
+ Assert.assertEquals(resultRecordId, accountRecordId);
+ }
+
+ @Test(groups = "slow")
+ public void testRetrieveAccountRecordIdFromAccountObject() throws IOException {
+ insertAccount();
+
+ final Long resultAccountRecordId = nonEntityDao.retrieveAccountRecordIdFromObject(accountId, ObjectType.ACCOUNT, null);
+ Assert.assertEquals(resultAccountRecordId, accountRecordId);
+ }
+
+ @Test(groups = "slow")
+ public void testRetrieveAccountRecordIdFromTagDefinitionObject() throws IOException {
+ insertTagDefinition();
+
+ final Long resultAccountRecordId = nonEntityDao.retrieveAccountRecordIdFromObject(tagDefinitionId, ObjectType.TAG_DEFINITION, null);
+ Assert.assertEquals(resultAccountRecordId, null);
+ }
+
+ // Not Tag_definition or account which are special
+ @Test(groups = "slow")
+ public void testRetrieveAccountRecordIdFromOtherObject() throws IOException {
+ insertTag();
+
+ final Long resultAccountRecordId = nonEntityDao.retrieveAccountRecordIdFromObject(tagId, ObjectType.TAG, null);
+ Assert.assertEquals(resultAccountRecordId, accountRecordId);
+ }
+
+ @Test(groups = "slow")
+ public void testRetrieveTenantRecordIdFromObject() throws IOException {
+ insertAccount();
+
+ final Long resultTenantRecordId = nonEntityDao.retrieveTenantRecordIdFromObject(accountId, ObjectType.ACCOUNT, null);
+ Assert.assertEquals(resultTenantRecordId, tenantRecordId);
+ }
+
+ @Test(groups = "slow")
+ public void testRetrieveTenantRecordIdFromTenantObject() throws IOException {
+ insertTenant();
+
+ final Long resultTenantRecordId = nonEntityDao.retrieveTenantRecordIdFromObject(tenantId, ObjectType.TENANT, null);
+ Assert.assertEquals(resultTenantRecordId, tenantRecordId);
+ }
+
+ private void insertAccount() throws IOException {
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ // Note: we always create an accounts table, see MysqlTestingHelper
+ handle.execute("insert into accounts (record_id, id, email, name, first_name_length, is_notified_for_invoices, created_date, created_by, updated_date, updated_by, tenant_record_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ accountRecordId, accountId.toString(), "zozo@tt.com", "zozo", 4, false, new Date(), "i", new Date(), "j", tenantRecordId);
+ return null;
+ }
+ });
+ }
+
+ private void insertHistoryAccount() throws IOException {
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ // Note: we always create an accounts table, see MysqlTestingHelper
+ handle.execute("insert into account_history (record_id, id, email, name, first_name_length, is_notified_for_invoices, created_date, created_by, updated_date, updated_by, tenant_record_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ accountRecordId, accountId.toString(), "zozo@tt.com", "zozo", 4, false, new Date(), "i", new Date(), "j", tenantRecordId);
+ return null;
+ }
+ });
+ }
+
+ private void insertTagDefinition() throws IOException {
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ // Note: we always create an accounts table, see MysqlTestingHelper
+ handle.execute("insert into tag_definitions (record_id, id, name, description, is_active, created_date, created_by, updated_date, updated_by, tenant_record_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ tagDefinitionRecordId, tagDefinitionId.toString(), "tagdef", "nothing", 1, new Date(), "i", new Date(), "j", 0);
+ return null;
+ }
+ });
+ }
+
+ private void insertTag() throws IOException {
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ // Note: we always create an accounts table, see MysqlTestingHelper
+ handle.execute("insert into tags (record_id, id, tag_definition_id, object_id, object_type, is_active, created_date, created_by, updated_date, updated_by, account_record_id, tenant_record_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ tagRecordId, tagId.toString(), tagDefinitionId.toString(), accountId.toString(), "ACCOUNT", 1, new Date(), "i", new Date(), "j", accountRecordId, 0);
+ return null;
+ }
+ });
+ }
+
+ private void insertTenant() throws IOException {
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ // Note: we always create an accounts table, see MysqlTestingHelper
+ handle.execute("insert into tenants (record_id, id, external_key, api_key, api_secret, api_salt, created_date, created_by, updated_date, updated_by) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ tenantRecordId, tenantId.toString(), "foo", "key", "secret", "salt", new Date(), "i", new Date(), "j");
+ return null;
+ }
+ });
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/dao/TestPagination.java b/util/src/test/java/org/killbill/billing/util/dao/TestPagination.java
new file mode 100644
index 0000000..502ecfd
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/dao/TestPagination.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.tag.dao.TagDefinitionModelDao;
+import org.killbill.billing.util.tag.dao.TagDefinitionSqlDao;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestPagination extends UtilTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow", description = "Test Pagination: basic SqlDAO and DAO calls")
+ public void testTagDefinitionsPagination() throws Exception {
+ final TagDefinitionSqlDao tagDefinitionSqlDao = dbi.onDemand(TagDefinitionSqlDao.class);
+
+ for (int i = 0; i < 10; i++) {
+ final String definitionName = "name-" + i;
+ final String description = "description-" + i;
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ tagDefinitionDao.create(definitionName, description, internalCallContext);
+ assertListenerStatus();
+ }
+
+ // Tests via SQL dao directly
+ Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.getAll(internalCallContext)).size(), 10);
+ Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.get(0L, 100L, "record_id", internalCallContext)).size(), 10);
+ Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.get(5L, 100L, "record_id", internalCallContext)).size(), 5);
+ Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.get(5L, 10L, "record_id", internalCallContext)).size(), 5);
+ Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.get(0L, 5L, "record_id", internalCallContext)).size(), 5);
+ for (int i = 0; i < 10; i++) {
+ final List<TagDefinitionModelDao> tagDefinitions = ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.get(0L, (long) i, "record_id", internalCallContext));
+ Assert.assertEquals(tagDefinitions.size(), i);
+
+ for (int j = 0; j < tagDefinitions.size(); j++) {
+ Assert.assertEquals(tagDefinitions.get(j).getName(), "name-" + j);
+ Assert.assertEquals(tagDefinitions.get(j).getDescription(), "description-" + j);
+ }
+ }
+
+ // Tests via DAO (to test EntityDaoBase)
+ Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.getAll(internalCallContext)).size(), 10);
+ Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.get(0L, 100L, internalCallContext)).size(), 10);
+ Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.get(5L, 100L, internalCallContext)).size(), 5);
+ Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.get(5L, 10L, internalCallContext)).size(), 5);
+ Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.get(0L, 5L, internalCallContext)).size(), 5);
+ for (int i = 0; i < 10; i++) {
+ final List<TagDefinitionModelDao> tagDefinitions = ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.get(0L, (long) i, internalCallContext));
+ Assert.assertEquals(tagDefinitions.size(), i);
+
+ for (int j = 0; j < tagDefinitions.size(); j++) {
+ Assert.assertEquals(tagDefinitions.get(j).getName(), "name-" + j);
+ Assert.assertEquals(tagDefinitions.get(j).getDescription(), "description-" + j);
+ }
+ }
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritance.java b/util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritance.java
new file mode 100644
index 0000000..55ca1d9
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritance.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.antlr.stringtemplate.StringTemplateGroup;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+
+import com.google.common.collect.ImmutableMap;
+
+public class TestStringTemplateInheritance extends UtilTestSuiteNoDB {
+
+ InputStream entityStream;
+ InputStream kombuchaStream;
+
+ @Override
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+ entityStream = this.getClass().getResourceAsStream("/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg");
+ kombuchaStream = this.getClass().getResourceAsStream("/org/killbill/billing/util/dao/Kombucha.sql.stg");
+ }
+
+ @Override
+ @AfterMethod(groups = "fast")
+ public void afterMethod() throws Exception {
+ super.afterMethod();
+ if (entityStream != null) {
+ entityStream.close();
+ }
+ if (kombuchaStream != null) {
+ kombuchaStream.close();
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testCheckQueries() throws Exception {
+ // From http://www.antlr.org/wiki/display/ST/ST+condensed+--+Templates+and+groups#STcondensed--Templatesandgroups-Withsupergroupfile:
+ // there is no mechanism for automatically loading a mentioned super-group file
+ new StringTemplateGroup(new InputStreamReader(entityStream));
+
+ final StringTemplateGroup kombucha = new StringTemplateGroup(new InputStreamReader(kombuchaStream));
+
+ // Verify non inherited template
+ Assert.assertEquals(kombucha.getInstanceOf("isIsTimeForKombucha").toString(), "select hour(current_timestamp()) = 17 as is_time;");
+
+ // Verify inherited templates
+ Assert.assertEquals(kombucha.getInstanceOf("getById").toString(), "select\n" +
+ " t.record_id\n" +
+ ", t.id\n" +
+ ", t.tea\n" +
+ ", t.mushroom\n" +
+ ", t.sugar\n" +
+ ", t.account_record_id\n" +
+ ", t.tenant_record_id\n" +
+ "from kombucha t\n" +
+ "where t.id = :id\n" +
+ "and t.tenant_record_id = :tenantRecordId\n" +
+ ";");
+ Assert.assertEquals(kombucha.getInstanceOf("getByRecordId").toString(), "select\n" +
+ " t.record_id\n" +
+ ", t.id\n" +
+ ", t.tea\n" +
+ ", t.mushroom\n" +
+ ", t.sugar\n" +
+ ", t.account_record_id\n" +
+ ", t.tenant_record_id\n" +
+ "from kombucha t\n" +
+ "where t.record_id = :recordId\n" +
+ "and t.tenant_record_id = :tenantRecordId\n" +
+ ";");
+ Assert.assertEquals(kombucha.getInstanceOf("getRecordId").toString(), "select\n" +
+ " t.record_id\n" +
+ "from kombucha t\n" +
+ "where t.id = :id\n" +
+ "and t.tenant_record_id = :tenantRecordId\n" +
+ ";");
+ Assert.assertEquals(kombucha.getInstanceOf("getHistoryRecordId").toString(), "select\n" +
+ " max(t.record_id)\n" +
+ "from kombucha_history t\n" +
+ "where t.target_record_id = :targetRecordId\n" +
+ "and t.tenant_record_id = :tenantRecordId\n" +
+ ";");
+ Assert.assertEquals(kombucha.getInstanceOf("getAll").toString(), "select\n" +
+ " t.record_id\n" +
+ ", t.id\n" +
+ ", t.tea\n" +
+ ", t.mushroom\n" +
+ ", t.sugar\n" +
+ ", t.account_record_id\n" +
+ ", t.tenant_record_id\n" +
+ "from kombucha t\n" +
+ "where t.tenant_record_id = :tenantRecordId\n" +
+ "order by t.record_id ASC\n" +
+ ";");
+ Assert.assertEquals(kombucha.getInstanceOf("get", ImmutableMap.<String, String>of("orderBy", "record_id", "offset", "3", "rowCount", "12")).toString(), "select\n" +
+ " t.record_id\n" +
+ ", t.id\n" +
+ ", t.tea\n" +
+ ", t.mushroom\n" +
+ ", t.sugar\n" +
+ ", t.account_record_id\n" +
+ ", t.tenant_record_id\n" +
+ "from kombucha t\n" +
+ "where t.tenant_record_id = :tenantRecordId\n" +
+ "order by t.record_id\n" +
+ "limit :offset, :rowCount\n" +
+ ";");
+ Assert.assertEquals(kombucha.getInstanceOf("test").toString(), "select\n" +
+ " t.record_id\n" +
+ ", t.id\n" +
+ ", t.tea\n" +
+ ", t.mushroom\n" +
+ ", t.sugar\n" +
+ ", t.account_record_id\n" +
+ ", t.tenant_record_id\n" +
+ "from kombucha t\n" +
+ "where t.tenant_record_id = :tenantRecordId\n" +
+ "limit 1\n" +
+ ";");
+ Assert.assertEquals(kombucha.getInstanceOf("addHistoryFromTransaction").toString(), "insert into kombucha_history (\n" +
+ " id\n" +
+ ", target_record_id\n" +
+ ", change_type\n" +
+ ", tea\n" +
+ ", mushroom\n" +
+ ", sugar\n" +
+ ", account_record_id\n" +
+ ", tenant_record_id\n" +
+ ")\n" +
+ "values (\n" +
+ " :id\n" +
+ ", :targetRecordId\n" +
+ ", :changeType\n" +
+ ", :tea\n" +
+ ", :mushroom\n" +
+ ", :sugar\n" +
+ ", :accountRecordId\n" +
+ ", :tenantRecordId\n" +
+ ")\n" +
+ ";");
+
+ Assert.assertEquals(kombucha.getInstanceOf("insertAuditFromTransaction").toString(), "insert into audit_log (\n" +
+ "id\n" +
+ ", table_name\n" +
+ ", target_record_id\n" +
+ ", change_type\n" +
+ ", created_by\n" +
+ ", reason_code\n" +
+ ", comments\n" +
+ ", user_token\n" +
+ ", created_date\n" +
+ ", account_record_id\n" +
+ ", tenant_record_id\n" +
+ ")\n" +
+ "values (\n" +
+ " :id\n" +
+ ", :tableName\n" +
+ ", :targetRecordId\n" +
+ ", :changeType\n" +
+ ", :createdBy\n" +
+ ", :reasonCode\n" +
+ ", :comments\n" +
+ ", :userToken\n" +
+ ", :createdDate\n" +
+ ", :accountRecordId\n" +
+ ", :tenantRecordId\n" +
+ ")\n" +
+ ";");
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritanceWithJdbi.java b/util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritanceWithJdbi.java
new file mode 100644
index 0000000..02c6361
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritanceWithJdbi.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.dao;
+
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
+
+public class TestStringTemplateInheritanceWithJdbi extends UtilTestSuiteWithEmbeddedDB {
+
+ private static interface Kombucha extends Entity {}
+
+ private static interface KombuchaModelDao extends EntityModelDao<Kombucha> {}
+
+ @EntitySqlDaoStringTemplate("/org/killbill/billing/util/dao/Kombucha.sql.stg")
+ private static interface KombuchaSqlDao extends EntitySqlDao<KombuchaModelDao, Kombucha> {
+
+ @SqlQuery
+ public boolean isIsTimeForKombucha();
+ }
+
+ @Test(groups = "slow")
+ public void testInheritQueries() throws Exception {
+ final KombuchaSqlDao dao = dbi.onDemand(KombuchaSqlDao.class);
+
+ // Verify non inherited template
+ Assert.assertEquals(dao.isIsTimeForKombucha(), clock.getUTCNow().getHourOfDay() == 17);
+
+ // Verify inherited templates
+ Assert.assertFalse(dao.getAll(internalCallContext).hasNext());
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/email/DefaultCatalogTranslationTest.java b/util/src/test/java/org/killbill/billing/util/email/DefaultCatalogTranslationTest.java
new file mode 100644
index 0000000..f281f3d
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/email/DefaultCatalogTranslationTest.java
@@ -0,0 +1,102 @@
+package org.killbill.billing.util.email;/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.Locale;
+import java.util.Map;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.template.translation.DefaultCatalogTranslator;
+import org.killbill.billing.util.template.translation.Translator;
+import org.killbill.billing.util.template.translation.TranslatorConfig;
+
+import com.google.common.collect.ImmutableMap;
+
+import static org.testng.Assert.assertEquals;
+
+public class DefaultCatalogTranslationTest extends UtilTestSuiteNoDB {
+
+ private Translator translation;
+
+ @Override
+ @BeforeClass(groups = "fast")
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ final ConfigSource configSource = new ConfigSource() {
+ private final Map<String, String> properties = ImmutableMap.<String, String>of("org.killbill.template.invoiceFormatterFactoryClass",
+ "org.killbill.billing.mock.MockInvoiceFormatterFactory");
+
+ @Override
+ public String getString(final String propertyName) {
+ return properties.get(propertyName);
+ }
+ };
+
+ final TranslatorConfig config = new ConfigurationObjectFactory(configSource).build(TranslatorConfig.class);
+ translation = new DefaultCatalogTranslator(config);
+ }
+
+ @Test(groups = "fast")
+ public void testInitialization() {
+ final String shotgunMonthly = "shotgun-monthly";
+ final String shotgunAnnual = "shotgun-annual";
+ final String badText = "Bad text";
+
+ assertEquals(translation.getTranslation(Locale.US, shotgunMonthly), "Monthly shotgun plan");
+ assertEquals(translation.getTranslation(Locale.US, shotgunAnnual), "Annual shotgun plan");
+ assertEquals(translation.getTranslation(Locale.US, badText), badText);
+
+ assertEquals(translation.getTranslation(Locale.CANADA_FRENCH, shotgunMonthly), "Fusil de chasse mensuel");
+ assertEquals(translation.getTranslation(Locale.CANADA_FRENCH, shotgunAnnual), "Fusil de chasse annuel");
+ assertEquals(translation.getTranslation(Locale.CANADA_FRENCH, badText), badText);
+
+ assertEquals(translation.getTranslation(Locale.CHINA, shotgunMonthly), "Monthly shotgun plan");
+ assertEquals(translation.getTranslation(Locale.CHINA, shotgunAnnual), "Annual shotgun plan");
+ assertEquals(translation.getTranslation(Locale.CHINA, badText), badText);
+ }
+
+ @Test(groups = "fast")
+ public void testExistingTranslation() {
+ // If the translation exists, return the translation
+ final String originalText = "shotgun-monthly";
+ assertEquals(translation.getTranslation(Locale.US, originalText), "Monthly shotgun plan");
+ }
+
+ @Test(groups = "fast")
+ public void testMissingTranslation() {
+ // If the translation is missing from the file, return the original text
+ final String originalText = "missing translation";
+ assertEquals(translation.getTranslation(Locale.US, originalText), originalText);
+ }
+
+ @Test(groups = "fast")
+ public void testMissingTranslationFileWithEnglishText() {
+ // If the translation file doesn't exist, return the "English" translation
+ final String originalText = "shotgun-monthly";
+ assertEquals(translation.getTranslation(Locale.CHINA, originalText), "Monthly shotgun plan");
+ }
+
+ @Test(groups = "fast")
+ public void testMissingFileAndText() {
+ // If the file is missing, and the "English" translation is missing, return the original text
+ final String originalText = "missing translation";
+ assertEquals(translation.getTranslation(Locale.CHINA, originalText), originalText);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/email/EmailSenderTest.java b/util/src/test/java/org/killbill/billing/util/email/EmailSenderTest.java
new file mode 100644
index 0000000..f73094b
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/email/EmailSenderTest.java
@@ -0,0 +1,46 @@
+package org.killbill.billing.util.email;/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+
+@Test(groups = "slow")
+public class EmailSenderTest extends UtilTestSuiteNoDB {
+
+ private EmailConfig config;
+
+ @BeforeClass
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ config = new ConfigurationObjectFactory(System.getProperties()).build(EmailConfig.class);
+ }
+
+ @Test(enabled = false)
+ public void testSendEmail() throws Exception {
+ final String html = "<html><body><h1>Test E-mail</h1></body></html>";
+ final List<String> recipients = new ArrayList<String>();
+ recipients.add("killbill.ning@gmail.com");
+
+ final EmailSender sender = new DefaultEmailSender(config);
+ sender.sendHTMLEmail(recipients, null, "Test message", html);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/entity/dao/MockEntityDaoBase.java b/util/src/test/java/org/killbill/billing/util/entity/dao/MockEntityDaoBase.java
new file mode 100644
index 0000000..8951a0e
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/entity/dao/MockEntityDaoBase.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity.dao;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.killbill.billing.BillingExceptionBase;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.DefaultPagination;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.Pagination;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class MockEntityDaoBase<M extends EntityModelDao<E>, E extends Entity, U extends BillingExceptionBase> implements EntityDao<M, E, U> {
+
+ protected static final AtomicLong autoIncrement = new AtomicLong(1);
+
+ protected final Map<UUID, Map<Long, M>> entities = new HashMap<UUID, Map<Long, M>>();
+
+ @Override
+ public void create(final M entity, final InternalCallContext context) throws U {
+ entities.put(entity.getId(), ImmutableMap.<Long, M>of(autoIncrement.incrementAndGet(), entity));
+ }
+
+ @Override
+ public Long getRecordId(final UUID id, final InternalTenantContext context) {
+ return entities.get(id).keySet().iterator().next();
+ }
+
+ @Override
+ public M getByRecordId(final Long recordId, final InternalTenantContext context) {
+ for (final Map<Long, M> cur : entities.values()) {
+ if (cur.keySet().iterator().next().equals(recordId)) {
+ cur.values().iterator().next();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public M getById(final UUID id, final InternalTenantContext context) {
+ return entities.get(id).values().iterator().next();
+ }
+
+ @Override
+ public Pagination<M> getAll(final InternalTenantContext context) {
+ final List<M> result = new ArrayList<M>();
+ for (final Map<Long, M> cur : entities.values()) {
+ result.add(cur.values().iterator().next());
+ }
+ return new DefaultPagination<M>(getCount(context), result.iterator());
+ }
+
+ @Override
+ public Pagination<M> get(final Long offset, final Long limit, final InternalTenantContext context) {
+ return DefaultPagination.<M>build(offset, limit, ImmutableList.<M>copyOf(getAll(context)));
+ }
+
+ @Override
+ public Long getCount(final InternalTenantContext context) {
+ return (long) entities.keySet().size();
+ }
+
+ public void update(final M entity, final InternalCallContext context) {
+ final Long entityRecordId = getRecordId(entity.getId(), context);
+ entities.get(entity.getId()).put(entityRecordId, entity);
+ }
+
+ public void delete(final M entity, final InternalCallContext context) {
+ entities.remove(entity.getId());
+ }
+
+ @Override
+ public void test(final InternalTenantContext context) {
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/entity/TestDefaultPagination.java b/util/src/test/java/org/killbill/billing/util/entity/TestDefaultPagination.java
new file mode 100644
index 0000000..a5d76a5
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/entity/TestDefaultPagination.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.entity;
+
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultPagination extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast", description = "Test Util: Pagination builder tests")
+ public void testDefaultPaginationBuilder() throws Exception {
+ Assert.assertEquals(DefaultPagination.<Integer>build(0L, 0L, ImmutableList.<Integer>of()), expectedOf(0L, 0L, 0L, ImmutableList.<Integer>of()));
+ Assert.assertEquals(DefaultPagination.<Integer>build(0L, 10L, ImmutableList.<Integer>of()), expectedOf(0L, 0L, 0L, ImmutableList.<Integer>of()));
+ Assert.assertEquals(DefaultPagination.<Integer>build(10L, 0L, ImmutableList.<Integer>of()), expectedOf(10L, 0L, 0L, ImmutableList.<Integer>of()));
+ Assert.assertEquals(DefaultPagination.<Integer>build(10L, 10L, ImmutableList.<Integer>of()), expectedOf(10L, 0L, 0L, ImmutableList.<Integer>of()));
+
+ Assert.assertEquals(DefaultPagination.<Integer>build(0L, 0L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(0L, 0L, 5L, ImmutableList.<Integer>of()));
+ Assert.assertEquals(DefaultPagination.<Integer>build(0L, 10L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(0L, 5L, 5L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)));
+ Assert.assertEquals(DefaultPagination.<Integer>build(4L, 10L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(4L, 1L, 5L, ImmutableList.<Integer>of(5)));
+
+ Assert.assertEquals(DefaultPagination.<Integer>build(0L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(0L, 3L, 5L, ImmutableList.<Integer>of(1, 2, 3)));
+ Assert.assertEquals(DefaultPagination.<Integer>build(1L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(1L, 3L, 5L, ImmutableList.<Integer>of(2, 3, 4)));
+ Assert.assertEquals(DefaultPagination.<Integer>build(2L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(2L, 3L, 5L, ImmutableList.<Integer>of(3, 4, 5)));
+ Assert.assertEquals(DefaultPagination.<Integer>build(3L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(3L, 2L, 5L, ImmutableList.<Integer>of(4, 5)));
+ Assert.assertEquals(DefaultPagination.<Integer>build(4L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(4L, 1L, 5L, ImmutableList.<Integer>of(5)));
+ Assert.assertEquals(DefaultPagination.<Integer>build(5L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(5L, 0L, 5L, ImmutableList.<Integer>of()));
+ }
+
+ private Pagination<Integer> expectedOf(final Long currentOffset, final Long totalNbRecords,
+ final Long maxNbRecords, final List<Integer> delegate) {
+ return new DefaultPagination<Integer>(currentOffset, Long.MAX_VALUE, totalNbRecords, maxNbRecords, delegate.iterator());
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/export/dao/TestCSVExportOutputStream.java b/util/src/test/java/org/killbill/billing/util/export/dao/TestCSVExportOutputStream.java
new file mode 100644
index 0000000..b2d2785
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/export/dao/TestCSVExportOutputStream.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.export.dao;
+
+import java.io.ByteArrayOutputStream;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.api.ColumnInfo;
+import org.killbill.billing.util.validation.DefaultColumnInfo;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class TestCSVExportOutputStream extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testSimpleGenerator() throws Exception {
+ final CSVExportOutputStream out = new CSVExportOutputStream(new ByteArrayOutputStream());
+
+ // Create the schema
+ final String tableName = UUID.randomUUID().toString();
+ out.newTable(tableName,
+ ImmutableList.<ColumnInfo>of(
+ new DefaultColumnInfo(tableName, "first_name", 0, 0, true, 0, "varchar"),
+ new DefaultColumnInfo(tableName, "last_name", 0, 0, true, 0, "varchar"),
+ new DefaultColumnInfo(tableName, "age", 0, 0, true, 0, "tinyint"))
+ );
+
+ // Write some data
+ out.write(ImmutableMap.<String, Object>of("first_name", "jean",
+ "last_name", "dupond",
+ "age", 35));
+ // Don't assume "ordering"
+ out.write(ImmutableMap.<String, Object>of("last_name", "dujardin",
+ "first_name", "jack",
+ "age", 40));
+ out.write(ImmutableMap.<String, Object>of("age", 12,
+ "first_name", "pierre",
+ "last_name", "schmitt"));
+ // Verify the numeric parsing
+ out.write(ImmutableMap.<String, Object>of("first_name", "stephane",
+ "last_name", "dupont",
+ "age", "30"));
+
+ Assert.assertEquals(out.toString(), "-- " + tableName + " first_name,last_name,age\n" +
+ "jean,dupond,35\n" +
+ "jack,dujardin,40\n" +
+ "pierre,schmitt,12\n" +
+ "stephane,dupont,30\n");
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java b/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java
new file mode 100644
index 0000000..7fa49c9
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.export.dao;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Date;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.api.DatabaseExportOutputStream;
+import org.killbill.billing.util.validation.dao.DatabaseSchemaDao;
+
+public class TestDatabaseExportDao extends UtilTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testExportSimpleData() throws Exception {
+ // Empty database
+ final String dump = getDump();
+ Assert.assertEquals(dump, "");
+
+ final String accountId = UUID.randomUUID().toString();
+ final String accountEmail = UUID.randomUUID().toString().substring(0, 4) + '@' + UUID.randomUUID().toString().substring(0, 4);
+ final String accountName = UUID.randomUUID().toString().substring(0, 4);
+ final int firstNameLength = 4;
+ final boolean isNotifiedForInvoices = false;
+ final Date createdDate = new Date(12421982000L);
+ final String createdBy = UUID.randomUUID().toString().substring(0, 4);
+ final Date updatedDate = new Date(382910622000L);
+ final String updatedBy = UUID.randomUUID().toString().substring(0, 4);
+
+ final String tableNameA = "test_database_export_dao_a";
+ final String tableNameB = "test_database_export_dao_b";
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ handle.execute("drop table if exists " + tableNameA);
+ handle.execute("create table " + tableNameA + "(record_id int(11) unsigned not null auto_increment," +
+ "a_column char default 'a'," +
+ "account_record_id int(11) unsigned not null," +
+ "tenant_record_id int(11) unsigned default 0," +
+ "primary key(record_id));");
+ handle.execute("drop table if exists " + tableNameB);
+ handle.execute("create table " + tableNameB + "(record_id int(11) unsigned not null auto_increment," +
+ "b_column char default 'b'," +
+ "account_record_id int(11) unsigned not null," +
+ "tenant_record_id int(11) unsigned default 0," +
+ "primary key(record_id));");
+ handle.execute("insert into " + tableNameA + " (account_record_id, tenant_record_id) values (?, ?)",
+ internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+ handle.execute("insert into " + tableNameB + " (account_record_id, tenant_record_id) values (?, ?)",
+ internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+
+ // Add row in accounts table
+ handle.execute("insert into accounts (record_id, id, email, name, first_name_length, is_notified_for_invoices, created_date, created_by, updated_date, updated_by, tenant_record_id) " +
+ "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ internalCallContext.getAccountRecordId(), accountId, accountEmail, accountName, firstNameLength, isNotifiedForInvoices, createdDate, createdBy, updatedDate, updatedBy, internalCallContext.getTenantRecordId());
+ return null;
+ }
+ });
+
+ // Verify new dump
+ final String newDump = getDump();
+ // Note: unclear why Jackson would quote the header?
+ Assert.assertEquals(newDump, "-- accounts record_id,id,external_key,email,name,first_name_length,currency,\"billing_cycle_day_local\",\"billing_cycle_day_utc\",payment_method_id,time_zone,locale,address1,address2,company_name,city,state_or_province,country,postal_code,phone,migrated,\"is_notified_for_invoices\",created_date,created_by,updated_date,updated_by,tenant_record_id\n" +
+ String.format("%s,\"%s\",,%s,%s,%s,,,,,,,,,,,,,,,false,%s,\"%s\",%s,\"%s\",%s,%s", internalCallContext.getAccountRecordId(), accountId, accountEmail, accountName, firstNameLength,
+ isNotifiedForInvoices, "1970-05-24T18:33:02.000+0000", createdBy, "1982-02-18T20:03:42.000+0000", updatedBy, internalCallContext.getTenantRecordId()) + "\n" +
+ "-- " + tableNameA + " record_id,a_column,account_record_id,tenant_record_id\n" +
+ "1,a," + internalCallContext.getAccountRecordId() + "," + internalCallContext.getTenantRecordId() + "\n" +
+ "-- " + tableNameB + " record_id,b_column,account_record_id,tenant_record_id\n" +
+ "1,b," + internalCallContext.getAccountRecordId() + "," + internalCallContext.getTenantRecordId() + "\n");
+ }
+
+ private String getDump() {
+ final DatabaseExportOutputStream out = new CSVExportOutputStream(new ByteArrayOutputStream());
+ dao.exportDataForAccount(out, internalCallContext);
+ return out.toString();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/globallocker/TestMysqlGlobalLocker.java b/util/src/test/java/org/killbill/billing/util/globallocker/TestMysqlGlobalLocker.java
new file mode 100644
index 0000000..0efbec4
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/globallocker/TestMysqlGlobalLocker.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.globallocker;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.TransactionCallback;
+import org.skife.jdbi.v2.TransactionStatus;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.commons.locker.GlobalLock;
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.commons.locker.LockFailedException;
+import org.killbill.commons.locker.mysql.MySqlGlobalLocker;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+
+public class TestMysqlGlobalLocker extends UtilTestSuiteWithEmbeddedDB {
+
+ // Used as a manual test to validate the simple DAO by stepping through that locking is done and release correctly
+ @Test(groups = "slow")
+ public void testSimpleLocking() throws IOException, LockFailedException {
+ final String lockName = UUID.randomUUID().toString();
+
+ final GlobalLock lock = locker.lockWithNumberOfTries(LockerType.ACCOUNT_FOR_INVOICE_PAYMENTS.toString(), lockName, 3);
+
+ dbi.inTransaction(new TransactionCallback<Void>() {
+ @Override
+ public Void inTransaction(final Handle conn, final TransactionStatus status)
+ throws Exception {
+ conn.execute("insert into dummy2 (dummy_id) values ('" + UUID.randomUUID().toString() + "')");
+ return null;
+ }
+ });
+ Assert.assertEquals(locker.isFree(LockerType.ACCOUNT_FOR_INVOICE_PAYMENTS.toString(), lockName), false);
+
+ boolean gotException = false;
+ try {
+ locker.lockWithNumberOfTries(LockerType.ACCOUNT_FOR_INVOICE_PAYMENTS.toString(), lockName, 1);
+ } catch (LockFailedException e) {
+ gotException = true;
+ }
+ Assert.assertTrue(gotException);
+
+ lock.release();
+
+ Assert.assertEquals(locker.isFree(LockerType.ACCOUNT_FOR_INVOICE_PAYMENTS.toString(), lockName), true);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/glue/TestUtilModule.java b/util/src/test/java/org/killbill/billing/util/glue/TestUtilModule.java
new file mode 100644
index 0000000..78593de
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/glue/TestUtilModule.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.mockito.Mockito;
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.subscription.api.timeline.SubscriptionBaseTimelineApi;
+
+import com.google.inject.AbstractModule;
+
+public class TestUtilModule extends AbstractModule {
+
+ protected final ConfigSource configSource;
+
+ public TestUtilModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ // TODO STEPH this is bad-- because DefaultAuditUserApi is using SubscriptionBaseTimeline API
+ public void installHack() {
+ bind(SubscriptionBaseTimelineApi.class).toInstance(Mockito.mock(SubscriptionBaseTimelineApi.class));
+ }
+
+ @Override
+ protected void configure() {
+ //install(new CallContextModule());
+ install(new CacheModule(configSource));
+
+ installHack();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/glue/TestUtilModuleNoDB.java b/util/src/test/java/org/killbill/billing/util/glue/TestUtilModuleNoDB.java
new file mode 100644
index 0000000..7d6852d
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/glue/TestUtilModuleNoDB.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.GuicyKillbillTestNoDBModule;
+import org.killbill.billing.mock.glue.MockGlobalLockerModule;
+import org.killbill.billing.mock.glue.MockNonEntityDaoModule;
+import org.killbill.billing.mock.glue.MockNotificationQueueModule;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.audit.api.DefaultAuditUserApi;
+import org.killbill.billing.util.audit.dao.AuditDao;
+import org.killbill.billing.util.audit.dao.MockAuditDao;
+import org.killbill.billing.util.bus.InMemoryBusModule;
+
+public class TestUtilModuleNoDB extends TestUtilModule {
+
+ public TestUtilModuleNoDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ private void installAuditMock() {
+ bind(AuditDao.class).toInstance(new MockAuditDao());
+ bind(AuditUserApi.class).to(DefaultAuditUserApi.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ install(new GuicyKillbillTestNoDBModule());
+
+ install(new MockNonEntityDaoModule());
+ install(new MockGlobalLockerModule());
+ install(new InMemoryBusModule(configSource));
+ install(new MockNotificationQueueModule(configSource));
+
+ installAuditMock();
+
+ install(new KillBillShiroModule(configSource));
+ install(new KillBillShiroAopModule());
+ install(new SecurityModule());
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/glue/TestUtilModuleWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/util/glue/TestUtilModuleWithEmbeddedDB.java
new file mode 100644
index 0000000..76c37f8
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/glue/TestUtilModuleWithEmbeddedDB.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.glue;
+
+import org.skife.config.ConfigSource;
+
+import org.killbill.billing.DBTestingHelper;
+import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
+import org.killbill.billing.api.TestApiListener;
+
+public class TestUtilModuleWithEmbeddedDB extends TestUtilModule {
+
+ public TestUtilModuleWithEmbeddedDB(final ConfigSource configSource) {
+ super(configSource);
+ }
+
+ @Override
+ protected void configure() {
+ super.configure();
+ install(new GuicyKillbillTestWithEmbeddedDBModule());
+
+ install(new AuditModule());
+ install(new TagStoreModule());
+ install(new CustomFieldModule());
+ install(new MetricsModule());
+ install(new BusModule(configSource));
+ install(new NotificationQueueModule(configSource));
+ install(new NonEntityDaoModule());
+ install(new GlobalLockerModule(DBTestingHelper.get().getDBEngine()));
+
+ bind(TestApiListener.class).asEagerSingleton();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/security/api/TestDefaultSecurityApi.java b/util/src/test/java/org/killbill/billing/util/security/api/TestDefaultSecurityApi.java
new file mode 100644
index 0000000..31ef1b2
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/security/api/TestDefaultSecurityApi.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.api;
+
+import java.util.Set;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.security.Permission;
+import org.killbill.billing.security.api.SecurityApi;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultSecurityApi extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testRetrievePermissions() throws Exception {
+ configureShiro();
+
+ // We don't want the Guice injected one (it has Shiro disabled)
+ final SecurityApi securityApi = new DefaultSecurityApi();
+
+ final Set<Permission> anonsPermissions = securityApi.getCurrentUserPermissions(callContext);
+ Assert.assertEquals(anonsPermissions.size(), 0);
+
+ login("pierre");
+ final Set<Permission> pierresPermissions = securityApi.getCurrentUserPermissions(callContext);
+ Assert.assertEquals(pierresPermissions.size(), 2);
+ Assert.assertTrue(pierresPermissions.containsAll(ImmutableList.<Permission>of(Permission.INVOICE_CAN_CREDIT, Permission.INVOICE_CAN_ITEM_ADJUST)));
+
+ login("stephane");
+ final Set<Permission> stephanesPermissions = securityApi.getCurrentUserPermissions(callContext);
+ Assert.assertEquals(stephanesPermissions.size(), 1);
+ Assert.assertTrue(stephanesPermissions.containsAll(ImmutableList.<Permission>of(Permission.PAYMENT_CAN_REFUND)));
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/security/shiro/dao/TestJDBCSessionDao.java b/util/src/test/java/org/killbill/billing/util/security/shiro/dao/TestJDBCSessionDao.java
new file mode 100644
index 0000000..45a424f
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/security/shiro/dao/TestJDBCSessionDao.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.dao;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.UUID;
+
+import org.apache.shiro.session.Session;
+import org.apache.shiro.session.mgt.SimpleSession;
+import org.skife.jdbi.v2.DBI;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+
+public class TestJDBCSessionDao extends UtilTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testCRUD() throws Exception {
+ // Note! We are testing the do* methods here to bypass the caching layer
+ final JDBCSessionDao jdbcSessionDao = new JDBCSessionDao(dbi);
+
+ // Retrieve
+ final SimpleSession session = createSession();
+ Assert.assertNull(jdbcSessionDao.doReadSession(session.getId()));
+
+ // Create
+ final Serializable sessionId = jdbcSessionDao.doCreate(session);
+ final Session retrievedSession = jdbcSessionDao.doReadSession(sessionId);
+ Assert.assertEquals(retrievedSession, session);
+
+ // Update
+ final String newHost = UUID.randomUUID().toString();
+ Assert.assertNotEquals(retrievedSession.getHost(), newHost);
+ session.setHost(newHost);
+ jdbcSessionDao.doUpdate(session);
+ Assert.assertEquals(jdbcSessionDao.doReadSession(sessionId).getHost(), newHost);
+
+ // Delete
+ jdbcSessionDao.doDelete(session);
+ Assert.assertNull(jdbcSessionDao.doReadSession(session.getId()));
+ }
+
+ private SimpleSession createSession() {
+ final SimpleSession simpleSession = new SimpleSession();
+ simpleSession.setStartTimestamp(new Date(System.currentTimeMillis() - 5000));
+ simpleSession.setLastAccessTime(new Date(System.currentTimeMillis()));
+ simpleSession.setTimeout(493934L);
+ simpleSession.setHost(UUID.randomUUID().toString());
+ simpleSession.setAttribute(UUID.randomUUID().toString(), Short.MIN_VALUE);
+ simpleSession.setAttribute(UUID.randomUUID().toString(), Integer.MIN_VALUE);
+ simpleSession.setAttribute(UUID.randomUUID().toString(), Long.MIN_VALUE);
+ simpleSession.setAttribute(UUID.randomUUID().toString(), UUID.randomUUID().toString());
+ // Test with Serializable objects
+ simpleSession.setAttribute(UUID.randomUUID().toString(), UUID.randomUUID());
+ simpleSession.setAttribute(UUID.randomUUID().toString(), new Date(1242));
+ return simpleSession;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/security/shiro/dao/TestSessionModelDao.java b/util/src/test/java/org/killbill/billing/util/security/shiro/dao/TestSessionModelDao.java
new file mode 100644
index 0000000..7f9d7db
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/security/shiro/dao/TestSessionModelDao.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.dao;
+
+import java.util.Date;
+import java.util.UUID;
+
+import org.apache.shiro.session.Session;
+import org.apache.shiro.session.mgt.SimpleSession;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+
+public class TestSessionModelDao extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testRoundTrip() throws Exception {
+ final SimpleSession simpleSession = new SimpleSession();
+ simpleSession.setStartTimestamp(new Date(System.currentTimeMillis() - 5000));
+ simpleSession.setLastAccessTime(new Date(System.currentTimeMillis()));
+ simpleSession.setTimeout(493934L);
+ simpleSession.setHost(UUID.randomUUID().toString());
+ simpleSession.setAttribute(UUID.randomUUID(), Short.MIN_VALUE);
+ simpleSession.setAttribute(UUID.randomUUID(), Integer.MIN_VALUE);
+ simpleSession.setAttribute(UUID.randomUUID(), Long.MIN_VALUE);
+ simpleSession.setAttribute(UUID.randomUUID().toString(), UUID.randomUUID().toString());
+ // Test with Serializable objects
+ simpleSession.setAttribute(UUID.randomUUID().toString(), UUID.randomUUID());
+ simpleSession.setAttribute(UUID.randomUUID().toString(), new Date(1242));
+
+ final SessionModelDao sessionModelDao = new SessionModelDao(simpleSession);
+ Assert.assertEquals(sessionModelDao.getTimeout(), simpleSession.getTimeout());
+ Assert.assertEquals(sessionModelDao.getHost(), simpleSession.getHost());
+ Assert.assertTrue(sessionModelDao.getSessionData().length > 0);
+
+ final Session retrievedSession = sessionModelDao.toSimpleSession();
+ Assert.assertEquals(retrievedSession, simpleSession);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJndiLdapRealm.java b/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJndiLdapRealm.java
new file mode 100644
index 0000000..262a5a7
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJndiLdapRealm.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.realm;
+
+import java.util.Collection;
+import java.util.Map;
+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.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.skife.config.SimplePropertyConfigSource;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.config.SecurityConfig;
+
+import com.google.common.collect.Sets;
+
+public class TestKillBillJndiLdapRealm extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testCheckConfiguration() throws Exception {
+ // Test default configuration (see SecurityConfig)
+ final Map<String, Collection<String>> permission = killBillJndiLdapRealm.getPermissionsByGroup();
+
+ Assert.assertEquals(permission.get("admin").size(), 1);
+ Assert.assertEquals(permission.get("admin").iterator().next(), "*:*");
+
+ Assert.assertEquals(permission.get("finance").size(), 2);
+ Assert.assertEquals(Sets.newHashSet(permission.get("finance")), Sets.newHashSet("invoice:*", "payment:*"));
+
+ Assert.assertEquals(permission.get("support").size(), 2);
+ Assert.assertEquals(Sets.newHashSet(permission.get("support")), Sets.newHashSet("entitlement:*", "invoice:item_adjust"));
+ }
+
+ @Test(groups = "external", enabled = false)
+ public void testCheckLDAPConnection() throws Exception {
+ // Convenience method to verify your LDAP connectivity
+ final Properties props = new Properties();
+ props.setProperty("org.killbill.security.ldap.userDnTemplate", "uid={0},ou=users,dc=mycompany,dc=com");
+ props.setProperty("org.killbill.security.ldap.searchBase", "ou=groups,dc=mycompany,dc=com");
+ props.setProperty("org.killbill.security.ldap.groupSearchFilter", "memberOf=uid={0},ou=users,dc=mycompany,dc=com");
+ props.setProperty("org.killbill.security.ldap.groupNameId", "cn");
+ props.setProperty("org.killbill.security.ldap.url", "ldap://ldap:389");
+ props.setProperty("org.killbill.security.ldap.disableSSLCheck", "true");
+ props.setProperty("org.killbill.security.ldap.systemUsername", "cn=root");
+ props.setProperty("org.killbill.security.ldap.systemPassword", "password");
+ props.setProperty("org.killbill.security.ldap.authenticationMechanism", "simple");
+ props.setProperty("org.killbill.security.ldap.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 KillBillJndiLdapRealm ldapRealm = new KillBillJndiLdapRealm(securityConfig);
+
+ final String username = "pierre";
+ final String password = "password";
+
+ // Check authentication
+ final UsernamePasswordToken token = new UsernamePasswordToken(username, password);
+ final AuthenticationInfo authenticationInfo = ldapRealm.getAuthenticationInfo(token);
+ System.out.println(authenticationInfo);
+
+ // Check permissions
+ final SimplePrincipalCollection principals = new SimplePrincipalCollection(username, username);
+ final AuthorizationInfo authorizationInfo = ldapRealm.queryForAuthorizationInfo(principals, ldapRealm.getContextFactory());
+ System.out.println("Roles: " + authorizationInfo.getRoles());
+ System.out.println("Permissions: " + authorizationInfo.getStringPermissions());
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/security/TestPermissionAnnotationMethodInterceptor.java b/util/src/test/java/org/killbill/billing/util/security/TestPermissionAnnotationMethodInterceptor.java
new file mode 100644
index 0000000..889a7a6
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/security/TestPermissionAnnotationMethodInterceptor.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security;
+
+import javax.inject.Singleton;
+
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.UnauthenticatedException;
+import org.mockito.Mockito;
+import org.skife.jdbi.v2.IDBI;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.security.Permission;
+import org.killbill.billing.security.RequiresPermissions;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.glue.KillBillShiroAopModule;
+import org.killbill.billing.util.glue.KillBillShiroModule;
+import org.killbill.billing.util.glue.SecurityModule;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+import net.sf.ehcache.CacheManager;
+
+public class TestPermissionAnnotationMethodInterceptor extends UtilTestSuiteNoDB {
+
+ public static interface IAopTester {
+
+ @RequiresPermissions(Permission.PAYMENT_CAN_REFUND)
+ public void createRefund();
+ }
+
+ public static class AopTesterImpl implements IAopTester {
+
+ @Override
+ public void createRefund() {}
+ }
+
+ @Singleton
+ public static class AopTester implements IAopTester {
+
+ @RequiresPermissions(Permission.PAYMENT_CAN_REFUND)
+ public void createRefund() {}
+ }
+
+ @Test(groups = "fast")
+ public void testAOPForClass() throws Exception {
+ // Make sure it works as expected without any AOP magic
+ final IAopTester simpleTester = new AopTester();
+ try {
+ simpleTester.createRefund();
+ } catch (Exception e) {
+ Assert.fail(e.getLocalizedMessage());
+ }
+
+ // Now, verify the interception works
+ configureShiro();
+ // Shutdown the cache manager to avoid duplicate exceptions
+ CacheManager.getInstance().shutdown();
+ final Injector injector = Guice.createInjector(Stage.PRODUCTION,
+ new KillBillShiroModule(configSource),
+ new KillBillShiroAopModule(),
+ new SecurityModule(),
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(IDBI.class).toInstance(Mockito.mock(IDBI.class));
+ }
+ });
+ final AopTester aopedTester = injector.getInstance(AopTester.class);
+ verifyAopedTester(aopedTester);
+ }
+
+ @Test(groups = "fast")
+ public void testAOPForInterface() throws Exception {
+ // Make sure it works as expected without any AOP magic
+ final IAopTester simpleTester = new AopTesterImpl();
+ try {
+ simpleTester.createRefund();
+ } catch (Exception e) {
+ Assert.fail(e.getLocalizedMessage());
+ }
+
+ // Now, verify the interception works
+ configureShiro();
+ // Shutdown the cache manager to avoid duplicate exceptions
+ CacheManager.getInstance().shutdown();
+ final Injector injector = Guice.createInjector(Stage.PRODUCTION,
+ new KillBillShiroModule(configSource),
+ new KillBillShiroAopModule(),
+ new SecurityModule(),
+ new AbstractModule() {
+ @Override
+ public void configure() {
+ bind(IDBI.class).toInstance(Mockito.mock(IDBI.class));
+ bind(IAopTester.class).to(AopTesterImpl.class).asEagerSingleton();
+ }
+ });
+ final IAopTester aopedTester = injector.getInstance(IAopTester.class);
+ verifyAopedTester(aopedTester);
+ }
+
+ private void verifyAopedTester(final IAopTester aopedTester) {
+ // Anonymous user
+ logout();
+ try {
+ aopedTester.createRefund();
+ Assert.fail();
+ } catch (UnauthenticatedException e) {
+ // Good!
+ } catch (Exception e) {
+ Assert.fail(e.getLocalizedMessage());
+ }
+
+ // pierre can credit, but not refund
+ login("pierre");
+ try {
+ aopedTester.createRefund();
+ Assert.fail();
+ } catch (AuthorizationException e) {
+ // Good!
+ } catch (Exception e) {
+ Assert.fail(e.getLocalizedMessage());
+ }
+
+ // stephane can refund
+ login("stephane");
+ aopedTester.createRefund();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagCreationEvent.java b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagCreationEvent.java
new file mode 100644
index 0000000..245a380
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagCreationEvent.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public class TestDefaultControlTagCreationEvent extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testPojo() throws Exception {
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultControlTagCreationEvent event = new DefaultControlTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID());
+ Assert.assertEquals(event.getBusEventType(), BusInternalEvent.BusInternalEventType.CONTROL_TAG_CREATION);
+
+ Assert.assertEquals(event.getTagId(), tagId);
+ Assert.assertEquals(event.getObjectId(), objectId);
+ Assert.assertEquals(event.getObjectType(), objectType);
+ Assert.assertEquals(event.getTagDefinition(), tagDefinition);
+ Assert.assertEquals(event.getTagDefinition().getId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition().getName(), tagDefinitionName);
+ Assert.assertEquals(event.getTagDefinition().getDescription(), tagDefinitionDescription);
+
+ Assert.assertEquals(event, event);
+ Assert.assertEquals(event, new DefaultControlTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(event));
+ Assert.assertTrue(event.equals(new DefaultControlTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID())));
+ }
+
+ @Test(groups = "fast")
+ public void testSerialization() throws Exception {
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultControlTagCreationEvent event = new DefaultControlTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID());
+
+ final String json = objectMapper.writeValueAsString(event);
+ final DefaultControlTagCreationEvent fromJson = objectMapper.readValue(json, DefaultControlTagCreationEvent.class);
+ Assert.assertEquals(fromJson, event);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDefinitionCreationEvent.java b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDefinitionCreationEvent.java
new file mode 100644
index 0000000..e64092f
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDefinitionCreationEvent.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public class TestDefaultControlTagDefinitionCreationEvent extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testPojo() throws Exception {
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultControlTagDefinitionCreationEvent event = new DefaultControlTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID());
+ Assert.assertEquals(event.getBusEventType(), BusInternalEvent.BusInternalEventType.CONTROL_TAGDEFINITION_CREATION);
+
+ Assert.assertEquals(event.getTagDefinitionId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition(), tagDefinition);
+ Assert.assertEquals(event.getTagDefinition().getId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition().getName(), tagDefinitionName);
+ Assert.assertEquals(event.getTagDefinition().getDescription(), tagDefinitionDescription);
+
+ Assert.assertEquals(event, event);
+ Assert.assertEquals(event, new DefaultControlTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(event));
+ Assert.assertTrue(event.equals(new DefaultControlTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID())));
+ }
+
+ @Test(groups = "fast")
+ public void testSerialization() throws Exception {
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultControlTagDefinitionCreationEvent event = new DefaultControlTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID());
+
+ final String json = objectMapper.writeValueAsString(event);
+ final DefaultControlTagDefinitionCreationEvent fromJson = objectMapper.readValue(json, DefaultControlTagDefinitionCreationEvent.class);
+ Assert.assertEquals(fromJson, event);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDefinitionDeletionEvent.java b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDefinitionDeletionEvent.java
new file mode 100644
index 0000000..f78a0aa
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDefinitionDeletionEvent.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public class TestDefaultControlTagDefinitionDeletionEvent extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testPojo() throws Exception {
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultControlTagDefinitionDeletionEvent event = new DefaultControlTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID());
+ Assert.assertEquals(event.getBusEventType(), BusInternalEvent.BusInternalEventType.CONTROL_TAGDEFINITION_DELETION);
+
+ Assert.assertEquals(event.getTagDefinitionId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition(), tagDefinition);
+ Assert.assertEquals(event.getTagDefinition().getId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition().getName(), tagDefinitionName);
+ Assert.assertEquals(event.getTagDefinition().getDescription(), tagDefinitionDescription);
+
+ Assert.assertEquals(event, event);
+ Assert.assertEquals(event, new DefaultControlTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(event));
+ Assert.assertTrue(event.equals(new DefaultControlTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID())));
+ }
+
+ @Test(groups = "fast")
+ public void testSerialization() throws Exception {
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultControlTagDefinitionDeletionEvent event = new DefaultControlTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID());
+
+ final String json = objectMapper.writeValueAsString(event);
+ final DefaultControlTagDefinitionDeletionEvent fromJson = objectMapper.readValue(json, DefaultControlTagDefinitionDeletionEvent.class);
+ Assert.assertEquals(fromJson, event);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDeletionEvent.java b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDeletionEvent.java
new file mode 100644
index 0000000..c9ff86c
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultControlTagDeletionEvent.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public class TestDefaultControlTagDeletionEvent extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testPojo() throws Exception {
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultControlTagDeletionEvent event = new DefaultControlTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID());
+ Assert.assertEquals(event.getBusEventType(), BusInternalEvent.BusInternalEventType.CONTROL_TAG_DELETION);
+
+ Assert.assertEquals(event.getTagId(), tagId);
+ Assert.assertEquals(event.getObjectId(), objectId);
+ Assert.assertEquals(event.getObjectType(), objectType);
+ Assert.assertEquals(event.getTagDefinition(), tagDefinition);
+ Assert.assertEquals(event.getTagDefinition().getId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition().getName(), tagDefinitionName);
+ Assert.assertEquals(event.getTagDefinition().getDescription(), tagDefinitionDescription);
+
+ Assert.assertEquals(event, event);
+ Assert.assertEquals(event, new DefaultControlTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(event));
+ Assert.assertTrue(event.equals(new DefaultControlTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID())));
+ }
+
+ @Test(groups = "fast")
+ public void testSerialization() throws Exception {
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultControlTagDeletionEvent event = new DefaultControlTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID());
+
+ final String json = objectMapper.writeValueAsString(event);
+ final DefaultControlTagDeletionEvent fromJson = objectMapper.readValue(json, DefaultControlTagDeletionEvent.class);
+ Assert.assertEquals(fromJson, event);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagCreationEvent.java b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagCreationEvent.java
new file mode 100644
index 0000000..2c98e20
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagCreationEvent.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public class TestDefaultUserTagCreationEvent extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testPojo() throws Exception {
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultUserTagCreationEvent event = new DefaultUserTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID());
+ Assert.assertEquals(event.getBusEventType(), BusInternalEvent.BusInternalEventType.USER_TAG_CREATION);
+
+ Assert.assertEquals(event.getTagId(), tagId);
+ Assert.assertEquals(event.getObjectId(), objectId);
+ Assert.assertEquals(event.getObjectType(), objectType);
+ Assert.assertEquals(event.getTagDefinition(), tagDefinition);
+ Assert.assertEquals(event.getTagDefinition().getId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition().getName(), tagDefinitionName);
+ Assert.assertEquals(event.getTagDefinition().getDescription(), tagDefinitionDescription);
+
+ Assert.assertEquals(event, event);
+ Assert.assertEquals(event, new DefaultUserTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(event));
+ Assert.assertTrue(event.equals(new DefaultUserTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID())));
+ }
+
+ @Test(groups = "fast")
+ public void testSerialization() throws Exception {
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultUserTagCreationEvent event = new DefaultUserTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID());
+
+ final String json = objectMapper.writeValueAsString(event);
+ final DefaultUserTagCreationEvent fromJson = objectMapper.readValue(json, DefaultUserTagCreationEvent.class);
+ Assert.assertEquals(fromJson, event);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDefinitionCreationEvent.java b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDefinitionCreationEvent.java
new file mode 100644
index 0000000..93bce4b
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDefinitionCreationEvent.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public class TestDefaultUserTagDefinitionCreationEvent extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testPojo() throws Exception {
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultUserTagDefinitionCreationEvent event = new DefaultUserTagDefinitionCreationEvent(tagDefinitionId, tagDefinition,
+ 1L, 2L, UUID.randomUUID());
+ Assert.assertEquals(event.getBusEventType(), BusInternalEvent.BusInternalEventType.USER_TAGDEFINITION_CREATION);
+
+ Assert.assertEquals(event.getTagDefinitionId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition(), tagDefinition);
+ Assert.assertEquals(event.getTagDefinition().getId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition().getName(), tagDefinitionName);
+ Assert.assertEquals(event.getTagDefinition().getDescription(), tagDefinitionDescription);
+
+ Assert.assertEquals(event, event);
+ Assert.assertEquals(event, new DefaultUserTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(event));
+ Assert.assertTrue(event.equals(new DefaultUserTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID())));
+ }
+
+ @Test(groups = "fast")
+ public void testSerialization() throws Exception {
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultUserTagDefinitionCreationEvent event = new DefaultUserTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID());
+
+ final String json = objectMapper.writeValueAsString(event);
+ final DefaultUserTagDefinitionCreationEvent fromJson = objectMapper.readValue(json, DefaultUserTagDefinitionCreationEvent.class);
+ Assert.assertEquals(fromJson, event);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDefinitionDeletionEvent.java b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDefinitionDeletionEvent.java
new file mode 100644
index 0000000..e80c620
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDefinitionDeletionEvent.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public class TestDefaultUserTagDefinitionDeletionEvent extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testPojo() throws Exception {
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultUserTagDefinitionDeletionEvent event = new DefaultUserTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID());
+ Assert.assertEquals(event.getBusEventType(), BusInternalEvent.BusInternalEventType.USER_TAGDEFINITION_DELETION);
+
+ Assert.assertEquals(event.getTagDefinitionId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition(), tagDefinition);
+ Assert.assertEquals(event.getTagDefinition().getId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition().getName(), tagDefinitionName);
+ Assert.assertEquals(event.getTagDefinition().getDescription(), tagDefinitionDescription);
+
+ Assert.assertEquals(event, event);
+ Assert.assertEquals(event, new DefaultUserTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(event));
+ Assert.assertTrue(event.equals(new DefaultUserTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID())));
+ }
+
+ @Test(groups = "fast")
+ public void testSerialization() throws Exception {
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultUserTagDefinitionDeletionEvent event = new DefaultUserTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID());
+
+ final String json = objectMapper.writeValueAsString(event);
+ final DefaultUserTagDefinitionDeletionEvent fromJson = objectMapper.readValue(json, DefaultUserTagDefinitionDeletionEvent.class);
+ Assert.assertEquals(fromJson, event);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDeletionEvent.java b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDeletionEvent.java
new file mode 100644
index 0000000..29b4314
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestDefaultUserTagDeletionEvent.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public class TestDefaultUserTagDeletionEvent extends UtilTestSuiteNoDB
+{
+ @Test(groups = "fast")
+ public void testPojo() throws Exception {
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultUserTagDeletionEvent event = new DefaultUserTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID());
+ Assert.assertEquals(event.getBusEventType(), BusInternalEvent.BusInternalEventType.USER_TAG_DELETION);
+
+ Assert.assertEquals(event.getTagId(), tagId);
+ Assert.assertEquals(event.getObjectId(), objectId);
+ Assert.assertEquals(event.getObjectType(), objectType);
+ Assert.assertEquals(event.getTagDefinition(), tagDefinition);
+ Assert.assertEquals(event.getTagDefinition().getId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition().getName(), tagDefinitionName);
+ Assert.assertEquals(event.getTagDefinition().getDescription(), tagDefinitionDescription);
+
+ Assert.assertEquals(event, event);
+ Assert.assertEquals(event, new DefaultUserTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(event));
+ Assert.assertTrue(event.equals(new DefaultUserTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID())));
+ }
+
+ @Test(groups = "fast")
+ public void testSerialization() throws Exception {
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultUserTagDeletionEvent event = new DefaultUserTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID());
+
+ final String json = objectMapper.writeValueAsString(event);
+ final DefaultUserTagDeletionEvent fromJson = objectMapper.readValue(json, DefaultUserTagDeletionEvent.class);
+ Assert.assertEquals(fromJson, event);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/user/TestTagEventBuilder.java b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestTagEventBuilder.java
new file mode 100644
index 0000000..4d64b23
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/user/TestTagEventBuilder.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.api.user;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.events.ControlTagCreationInternalEvent;
+import org.killbill.billing.events.ControlTagDefinitionCreationInternalEvent;
+import org.killbill.billing.events.ControlTagDefinitionDeletionInternalEvent;
+import org.killbill.billing.events.ControlTagDeletionInternalEvent;
+import org.killbill.billing.events.TagDefinitionInternalEvent;
+import org.killbill.billing.events.TagInternalEvent;
+import org.killbill.billing.events.UserTagCreationInternalEvent;
+import org.killbill.billing.events.UserTagDefinitionCreationInternalEvent;
+import org.killbill.billing.events.UserTagDefinitionDeletionInternalEvent;
+import org.killbill.billing.events.UserTagDeletionInternalEvent;
+import org.killbill.billing.util.tag.DefaultTagDefinition;
+import org.killbill.billing.util.tag.TagDefinition;
+import org.killbill.billing.util.tag.dao.TagDefinitionModelDao;
+
+public class TestTagEventBuilder extends UtilTestSuiteNoDB {
+
+ @Test(groups = "fast")
+ public void testNewUserTagDefinitionCreationEvent() throws Exception {
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = false;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = internalCallContext.getUserToken();
+
+ final TagEventBuilder tagEventBuilder = new TagEventBuilder();
+ final TagDefinitionInternalEvent event = tagEventBuilder.newUserTagDefinitionCreationEvent(tagDefinitionId, new TagDefinitionModelDao(tagDefinition), 1L, 2L, UUID.randomUUID());
+ Assert.assertTrue(event instanceof UserTagDefinitionCreationInternalEvent);
+
+ Assert.assertEquals(event, new DefaultUserTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID()));
+
+ Assert.assertTrue(event.equals(new DefaultUserTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID())));
+
+ verifyTagDefinitionEvent(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, tagDefinition, userToken, event);
+ }
+
+ @Test(groups = "fast")
+ public void testNewUserTagDefinitionDeletionEvent() throws Exception {
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = false;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = internalCallContext.getUserToken();
+
+ final TagEventBuilder tagEventBuilder = new TagEventBuilder();
+ final TagDefinitionInternalEvent event = tagEventBuilder.newUserTagDefinitionDeletionEvent(tagDefinitionId, new TagDefinitionModelDao(tagDefinition), 1L, 2L, UUID.randomUUID());
+ Assert.assertTrue(event instanceof UserTagDefinitionDeletionInternalEvent);
+
+ Assert.assertEquals(event, new DefaultUserTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(new DefaultUserTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID())));
+
+ verifyTagDefinitionEvent(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, tagDefinition, userToken, event);
+ }
+
+ @Test(groups = "fast")
+ public void testNewControlTagDefinitionCreationEvent() throws Exception {
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = internalCallContext.getUserToken();
+
+ final TagEventBuilder tagEventBuilder = new TagEventBuilder();
+ final TagDefinitionInternalEvent event = tagEventBuilder.newControlTagDefinitionCreationEvent(tagDefinitionId, new TagDefinitionModelDao(tagDefinition), 1L, 2L, UUID.randomUUID());
+ Assert.assertTrue(event instanceof ControlTagDefinitionCreationInternalEvent);
+
+ Assert.assertEquals(event, new DefaultControlTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(new DefaultControlTagDefinitionCreationEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID())));
+
+ verifyTagDefinitionEvent(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, tagDefinition, userToken, event);
+ }
+
+ @Test(groups = "fast")
+ public void testNewControlTagDefinitionDeletionEvent() throws Exception {
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = internalCallContext.getUserToken();
+
+ final TagEventBuilder tagEventBuilder = new TagEventBuilder();
+ final TagDefinitionInternalEvent event = tagEventBuilder.newControlTagDefinitionDeletionEvent(tagDefinitionId, new TagDefinitionModelDao(tagDefinition), 1L, 2L, UUID.randomUUID());
+ Assert.assertTrue(event instanceof ControlTagDefinitionDeletionInternalEvent);
+
+ Assert.assertEquals(event, new DefaultControlTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(new DefaultControlTagDefinitionDeletionEvent(tagDefinitionId, tagDefinition, 1L, 2L, UUID.randomUUID())));
+
+ verifyTagDefinitionEvent(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, tagDefinition, userToken, event);
+ }
+
+ @Test(groups = "fast")
+ public void testNewUserTagCreationEvent() throws Exception {
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = false;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = internalCallContext.getUserToken();
+
+ final TagEventBuilder tagEventBuilder = new TagEventBuilder();
+ final TagInternalEvent event = tagEventBuilder.newUserTagCreationEvent(tagId, objectId, objectType, new TagDefinitionModelDao(tagDefinition), 1L, 2L, UUID.randomUUID());
+ Assert.assertTrue(event instanceof UserTagCreationInternalEvent);
+
+ Assert.assertEquals(event, new DefaultUserTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(new DefaultUserTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID())));
+
+ verifyTagEvent(tagId, objectId, objectType, tagDefinitionId, tagDefinitionName, tagDefinitionDescription, tagDefinition, userToken, event);
+ }
+
+ @Test(groups = "fast")
+ public void testNewUserTagDeletionEvent() throws Exception {
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = false;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = internalCallContext.getUserToken();
+
+ final TagEventBuilder tagEventBuilder = new TagEventBuilder();
+ final TagInternalEvent event = tagEventBuilder.newUserTagDeletionEvent(tagId, objectId, objectType, new TagDefinitionModelDao(tagDefinition), 1L, 2L, UUID.randomUUID());
+ Assert.assertTrue(event instanceof UserTagDeletionInternalEvent);
+
+ Assert.assertEquals(event, new DefaultUserTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(new DefaultUserTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID())));
+
+ verifyTagEvent(tagId, objectId, objectType, tagDefinitionId, tagDefinitionName, tagDefinitionDescription, tagDefinition, userToken, event);
+ }
+
+ @Test(groups = "fast")
+ public void testNewControlTagCreationEvent() throws Exception {
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = internalCallContext.getUserToken();
+
+ final TagEventBuilder tagEventBuilder = new TagEventBuilder();
+ final TagInternalEvent event = tagEventBuilder.newControlTagCreationEvent(tagId, objectId, objectType, new TagDefinitionModelDao(tagDefinition), 1L, 2L, UUID.randomUUID());
+ Assert.assertTrue(event instanceof ControlTagCreationInternalEvent);
+
+ Assert.assertEquals(event, new DefaultControlTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(new DefaultControlTagCreationEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID())));
+
+ verifyTagEvent(tagId, objectId, objectType, tagDefinitionId, tagDefinitionName, tagDefinitionDescription, tagDefinition, userToken, event);
+ }
+
+ @Test(groups = "fast")
+ public void testNewControlTagDeletionEvent() throws Exception {
+ final UUID tagId = UUID.randomUUID();
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.ACCOUNT_EMAIL;
+ final UUID tagDefinitionId = UUID.randomUUID();
+ final String tagDefinitionName = UUID.randomUUID().toString();
+ final String tagDefinitionDescription = UUID.randomUUID().toString();
+ final boolean controlTag = true;
+ final TagDefinition tagDefinition = new DefaultTagDefinition(tagDefinitionId, tagDefinitionName, tagDefinitionDescription, controlTag);
+ final UUID userToken = internalCallContext.getUserToken();
+
+ final TagEventBuilder tagEventBuilder = new TagEventBuilder();
+ final TagInternalEvent event = tagEventBuilder.newControlTagDeletionEvent(tagId, objectId, objectType, new TagDefinitionModelDao(tagDefinition), 1L, 2L, UUID.randomUUID());
+ Assert.assertTrue(event instanceof ControlTagDeletionInternalEvent);
+
+ Assert.assertEquals(event, new DefaultControlTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID()));
+ Assert.assertTrue(event.equals(new DefaultControlTagDeletionEvent(tagId, objectId, objectType, tagDefinition, 1L, 2L, UUID.randomUUID())));
+
+ verifyTagEvent(tagId, objectId, objectType, tagDefinitionId, tagDefinitionName, tagDefinitionDescription, tagDefinition, userToken, event);
+ }
+
+ private void verifyTagDefinitionEvent(final UUID tagDefinitionId, final String tagDefinitionName, final String tagDefinitionDescription, final TagDefinition tagDefinition, final UUID userToken, final TagDefinitionInternalEvent event) {
+ Assert.assertEquals(event.getTagDefinitionId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition(), tagDefinition);
+ Assert.assertEquals(event.getTagDefinition().getId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition().getName(), tagDefinitionName);
+ Assert.assertEquals(event.getTagDefinition().getDescription(), tagDefinitionDescription);
+
+ Assert.assertEquals(event, event);
+ Assert.assertTrue(event.equals(event));
+ }
+
+ private void verifyTagEvent(final UUID tagId, final UUID objectId, final ObjectType objectType, final UUID tagDefinitionId, final String tagDefinitionName, final String tagDefinitionDescription, final TagDefinition tagDefinition, final UUID userToken, final TagInternalEvent event) {
+ Assert.assertEquals(event.getTagId(), tagId);
+ Assert.assertEquals(event.getObjectId(), objectId);
+ Assert.assertEquals(event.getObjectType(), objectType);
+ Assert.assertEquals(event.getTagDefinition(), tagDefinition);
+ Assert.assertEquals(event.getTagDefinition().getId(), tagDefinitionId);
+ Assert.assertEquals(event.getTagDefinition().getName(), tagDefinitionName);
+ Assert.assertEquals(event.getTagDefinition().getDescription(), tagDefinitionDescription);
+
+ Assert.assertEquals(event, event);
+ Assert.assertTrue(event.equals(event));
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/dao/MockTagDao.java b/util/src/test/java/org/killbill/billing/util/tag/dao/MockTagDao.java
new file mode 100644
index 0000000..dcbe233
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/dao/MockTagDao.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.MockEntityDaoBase;
+import org.killbill.billing.util.tag.Tag;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+public class MockTagDao extends MockEntityDaoBase<TagModelDao, Tag, TagApiException> implements TagDao {
+
+ private final Map<UUID, List<TagModelDao>> tagStore = new HashMap<UUID, List<TagModelDao>>();
+
+ @Override
+ public void create(final TagModelDao tag, final InternalCallContext context) throws TagApiException {
+ if (tagStore.get(tag.getObjectId()) == null) {
+ tagStore.put(tag.getObjectId(), new ArrayList<TagModelDao>());
+ }
+ tagStore.get(tag.getObjectId()).add(tag);
+ }
+
+ @Override
+ public void deleteTag(final UUID objectId, final ObjectType objectType,
+ final UUID tagDefinitionId, final InternalCallContext context) {
+ final List<TagModelDao> tags = tagStore.get(objectId);
+ if (tags != null) {
+ final Iterator<TagModelDao> tagIterator = tags.iterator();
+ while (tagIterator.hasNext()) {
+ final TagModelDao tag = tagIterator.next();
+ if (tag.getTagDefinitionId().equals(tagDefinitionId)) {
+ tagIterator.remove();
+ }
+ }
+ }
+ }
+
+ @Override
+ public Pagination<TagModelDao> searchTags(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public TagModelDao getById(final UUID tagId, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<TagModelDao> getTagsForObject(final UUID objectId, final ObjectType objectType, final boolean includedDeleted, final InternalTenantContext internalTenantContext) {
+ if (tagStore.get(objectId) == null) {
+ return ImmutableList.<TagModelDao>of();
+ }
+
+ return ImmutableList.<TagModelDao>copyOf(Collections2.filter(tagStore.get(objectId), new Predicate<TagModelDao>() {
+ @Override
+ public boolean apply(final TagModelDao input) {
+ return objectType.equals(input.getObjectType());
+ }
+ }));
+ }
+
+ @Override
+ public List<TagModelDao> getTagsForAccountType(final UUID accountId, final ObjectType objectType, final boolean includedDeleted, final InternalTenantContext internalTenantContext) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<TagModelDao> getTagsForAccount(final boolean includedDeleted, final InternalTenantContext internalTenantContext) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void clear() {
+ tagStore.clear();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/dao/MockTagDefinitionDao.java b/util/src/test/java/org/killbill/billing/util/tag/dao/MockTagDefinitionDao.java
new file mode 100644
index 0000000..c602091
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/dao/MockTagDefinitionDao.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.entity.dao.MockEntityDaoBase;
+import org.killbill.billing.util.tag.TagDefinition;
+
+public class MockTagDefinitionDao extends MockEntityDaoBase<TagDefinitionModelDao, TagDefinition, TagDefinitionApiException> implements TagDefinitionDao {
+
+ private final Map<String, TagDefinitionModelDao> tags = new ConcurrentHashMap<String, TagDefinitionModelDao>();
+
+ @Override
+ public List<TagDefinitionModelDao> getTagDefinitions(final InternalTenantContext context) {
+ return new ArrayList<TagDefinitionModelDao>(tags.values());
+ }
+
+ @Override
+ public TagDefinitionModelDao getByName(final String definitionName, final InternalTenantContext context) {
+ return tags.get(definitionName);
+ }
+
+ @Override
+ public TagDefinitionModelDao create(final String definitionName, final String description,
+ final InternalCallContext context) throws TagDefinitionApiException {
+ final TagDefinitionModelDao tag = new TagDefinitionModelDao(null, definitionName, description);
+
+ tags.put(tag.getId().toString(), tag);
+ return tag;
+ }
+
+ @Override
+ public void deleteById(final UUID definitionId, final InternalCallContext context) throws TagDefinitionApiException {
+ tags.remove(definitionId.toString());
+ }
+
+ @Override
+ public List<TagDefinitionModelDao> getByIds(final Collection<UUID> definitionIds, final InternalTenantContext context) {
+ return null;
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/dao/TestDefaultTagDao.java b/util/src/test/java/org/killbill/billing/util/tag/dao/TestDefaultTagDao.java
new file mode 100644
index 0000000..196ec17
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/dao/TestDefaultTagDao.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.tag.ControlTagType;
+import org.killbill.billing.util.tag.DescriptiveTag;
+import org.killbill.billing.util.tag.Tag;
+
+import static org.testng.Assert.assertEquals;
+
+public class TestDefaultTagDao extends UtilTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testGetByIds() throws TagDefinitionApiException {
+ final List<UUID> uuids = new ArrayList<UUID>();
+
+ // Check with a empty Collection first
+ List<TagDefinitionModelDao> result = tagDefinitionDao.getByIds(uuids, internalCallContext);
+ assertEquals(result.size(), 0);
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ final TagDefinitionModelDao defYo = tagDefinitionDao.create(UUID.randomUUID().toString().substring(0, 5), "defintion yo", internalCallContext);
+ assertListenerStatus();
+ uuids.add(defYo.getId());
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ final TagDefinitionModelDao defBah = tagDefinitionDao.create(UUID.randomUUID().toString().substring(0, 5), "defintion bah", internalCallContext);
+ assertListenerStatus();
+ uuids.add(defBah.getId());
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ final TagDefinitionModelDao defZoo = tagDefinitionDao.create(UUID.randomUUID().toString().substring(0, 5), "defintion zoo", internalCallContext);
+ assertListenerStatus();
+ uuids.add(defZoo.getId());
+
+ result = tagDefinitionDao.getByIds(uuids, internalCallContext);
+ assertEquals(result.size(), 3);
+
+ // Add control tag and retry
+ uuids.add(ControlTagType.AUTO_PAY_OFF.getId());
+ result = tagDefinitionDao.getByIds(uuids, internalCallContext);
+ assertEquals(result.size(), 4);
+
+ result = tagDefinitionDao.getTagDefinitions(internalCallContext);
+ assertEquals(result.size(), 3 + ControlTagType.values().length);
+ }
+
+ @Test(groups = "slow")
+ public void testGetById() throws TagDefinitionApiException {
+ // User Tag
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ final TagDefinitionModelDao defYo = tagDefinitionDao.create(UUID.randomUUID().toString().substring(0, 5), "defintion yo", internalCallContext);
+ assertListenerStatus();
+
+ final TagDefinitionModelDao resDefYo = tagDefinitionDao.getById(defYo.getId(), internalCallContext);
+ assertEquals(defYo, resDefYo);
+
+ // Control Tag
+ try {
+ tagDefinitionDao.create(ControlTagType.AUTO_INVOICING_OFF.name(), ControlTagType.AUTO_INVOICING_OFF.name(), internalCallContext);
+ Assert.fail("Should not be able to create a control tag");
+ } catch (TagDefinitionApiException ignore) {
+ }
+ final TagDefinitionModelDao resdef_AUTO_INVOICING_OFF = tagDefinitionDao.getById(ControlTagType.AUTO_INVOICING_OFF.getId(), internalCallContext);
+ assertEquals(resdef_AUTO_INVOICING_OFF.getId(), ControlTagType.AUTO_INVOICING_OFF.getId());
+ assertEquals(resdef_AUTO_INVOICING_OFF.getName(), ControlTagType.AUTO_INVOICING_OFF.name());
+ assertEquals(resdef_AUTO_INVOICING_OFF.getDescription(), ControlTagType.AUTO_INVOICING_OFF.getDescription());
+ }
+
+ @Test(groups = "slow")
+ public void testGetByName() throws TagDefinitionApiException {
+ // User Tag
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ final TagDefinitionModelDao defYo = tagDefinitionDao.create(UUID.randomUUID().toString().substring(0, 5), "defintion yo", internalCallContext);
+ assertListenerStatus();
+
+ final TagDefinitionModelDao resDefYo = tagDefinitionDao.getByName(defYo.getName(), internalCallContext);
+ assertEquals(defYo, resDefYo);
+
+ // Control Tag
+ try {
+ tagDefinitionDao.create(ControlTagType.AUTO_PAY_OFF.name(), ControlTagType.AUTO_INVOICING_OFF.name(), internalCallContext);
+ Assert.fail("Should not be able to create a control tag");
+ } catch (TagDefinitionApiException ignore) {
+ }
+ final TagDefinitionModelDao resdef_AUTO_PAY_OFF = tagDefinitionDao.getByName(ControlTagType.AUTO_PAY_OFF.name(), internalCallContext);
+ assertEquals(resdef_AUTO_PAY_OFF.getId(), ControlTagType.AUTO_PAY_OFF.getId());
+ assertEquals(resdef_AUTO_PAY_OFF.getName(), ControlTagType.AUTO_PAY_OFF.name());
+ assertEquals(resdef_AUTO_PAY_OFF.getDescription(), ControlTagType.AUTO_PAY_OFF.getDescription());
+ }
+
+ @Test(groups = "slow")
+ public void testCatchEventsOnCreateAndDelete() throws Exception {
+ final String definitionName = UUID.randomUUID().toString().substring(0, 5);
+ final String description = UUID.randomUUID().toString().substring(0, 5);
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.INVOICE_ITEM;
+
+ // Create a tag definition
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ final TagDefinitionModelDao createdTagDefinition = tagDefinitionDao.create(definitionName, description, internalCallContext);
+ Assert.assertEquals(createdTagDefinition.getName(), definitionName);
+ Assert.assertEquals(createdTagDefinition.getDescription(), description);
+ assertListenerStatus();
+
+ // Make sure we can create a tag
+ eventsListener.pushExpectedEvent(NextEvent.TAG);
+ final Tag tag = new DescriptiveTag(createdTagDefinition.getId(), objectType, objectId, internalCallContext.getCreatedDate());
+ tagDao.create(new TagModelDao(tag), internalCallContext);
+ assertListenerStatus();
+
+ // Make sure we can retrieve it via the DAO
+ final List<TagModelDao> foundTags = tagDao.getTagsForObject(objectId, objectType, false, internalCallContext);
+ Assert.assertEquals(foundTags.size(), 1);
+ Assert.assertEquals(foundTags.get(0).getTagDefinitionId(), createdTagDefinition.getId());
+ final List<TagModelDao> foundTagsForAccount = tagDao.getTagsForAccount(false, internalCallContext);
+ Assert.assertEquals(foundTagsForAccount.size(), 1);
+ Assert.assertEquals(foundTagsForAccount.get(0).getTagDefinitionId(), createdTagDefinition.getId());
+
+ // Delete the tag
+ eventsListener.pushExpectedEvent(NextEvent.TAG);
+ tagDao.deleteTag(objectId, objectType, createdTagDefinition.getId(), internalCallContext);
+ assertListenerStatus();
+
+ // Make sure the tag is deleted
+ Assert.assertEquals(tagDao.getTagsForObject(objectId, objectType, false, internalCallContext).size(), 0);
+ Assert.assertEquals(tagDao.getTagsForAccount(false, internalCallContext).size(), 0);
+ Assert.assertEquals(tagDao.getTagsForObject(objectId, objectType, true, internalCallContext).size(), 1);
+ Assert.assertEquals(tagDao.getTagsForAccount(true, internalCallContext).size(), 1);
+ }
+
+ @Test(groups = "slow")
+ public void testInsertMultipleTags() throws TagApiException {
+ final UUID objectId = UUID.randomUUID();
+ final ObjectType objectType = ObjectType.INVOICE_ITEM;
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG);
+ final Tag tag = new DescriptiveTag(ControlTagType.AUTO_INVOICING_OFF.getId(), objectType, objectId, internalCallContext.getCreatedDate());
+ tagDao.create(new TagModelDao(tag), internalCallContext);
+ assertListenerStatus();
+
+ try {
+ final Tag tag2 = new DescriptiveTag(ControlTagType.AUTO_INVOICING_OFF.getId(), objectType, objectId, internalCallContext.getCreatedDate());
+ tagDao.create(new TagModelDao(tag2), internalCallContext);
+ Assert.fail("Should not be able to create twice the same tag");
+ assertListenerStatus();
+ } catch (final TagApiException e) {
+ Assert.assertEquals(ErrorCode.TAG_ALREADY_EXISTS.getCode(), e.getCode());
+ }
+ }
+
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/dao/TestDefaultTagDefinitionDao.java b/util/src/test/java/org/killbill/billing/util/tag/dao/TestDefaultTagDefinitionDao.java
new file mode 100644
index 0000000..a24ba3e
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/dao/TestDefaultTagDefinitionDao.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.events.BusInternalEvent;
+import org.killbill.billing.events.TagDefinitionInternalEvent;
+
+import com.google.common.eventbus.Subscribe;
+
+public class TestDefaultTagDefinitionDao extends UtilTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testCatchEventsOnCreateAndDelete() throws Exception {
+ final String definitionName = UUID.randomUUID().toString().substring(0, 5);
+ final String description = UUID.randomUUID().toString().substring(0, 5);
+
+ // Make sure we can create a tag definition
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ final TagDefinitionModelDao createdTagDefinition = tagDefinitionDao.create(definitionName, description, internalCallContext);
+ Assert.assertEquals(createdTagDefinition.getName(), definitionName);
+ Assert.assertEquals(createdTagDefinition.getDescription(), description);
+ assertListenerStatus();
+
+ // Make sure we can retrieve it via the DAO
+ final TagDefinitionModelDao foundTagDefinition = tagDefinitionDao.getByName(definitionName, internalCallContext);
+ Assert.assertEquals(foundTagDefinition, createdTagDefinition);
+
+ /*
+ // Verify we caught an event on the bus
+ final TagDefinitionInternalEvent tagDefinitionFirstEventReceived = eventsListener.getTagDefinitionEvents().get(0);
+ Assert.assertEquals(tagDefinitionFirstEventReceived.getTagDefinitionId(), createdTagDefinition.getId());
+ Assert.assertEquals(tagDefinitionFirstEventReceived.getTagDefinition().getName(), createdTagDefinition.getName());
+ Assert.assertEquals(tagDefinitionFirstEventReceived.getTagDefinition().getDescription(), createdTagDefinition.getDescription());
+ Assert.assertEquals(tagDefinitionFirstEventReceived.getBusEventType(), BusInternalEvent.BusInternalEventType.USER_TAGDEFINITION_CREATION);
+ Assert.assertEquals(tagDefinitionFirstEventReceived.getUserToken(), internalCallContext.getUserToken());
+
+ */
+ // Delete the tag definition
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ tagDefinitionDao.deleteById(foundTagDefinition.getId(), internalCallContext);
+ assertListenerStatus();
+
+ // Make sure the tag definition is deleted
+ Assert.assertNull(tagDefinitionDao.getByName(definitionName, internalCallContext));
+
+ /*
+ // Verify we caught an event on the bus
+ final TagDefinitionInternalEvent tagDefinitionSecondEventReceived = eventsListener.getTagDefinitionEvents().get(1);
+ Assert.assertEquals(tagDefinitionSecondEventReceived.getTagDefinitionId(), createdTagDefinition.getId());
+ Assert.assertEquals(tagDefinitionSecondEventReceived.getTagDefinition().getName(), createdTagDefinition.getName());
+ Assert.assertEquals(tagDefinitionSecondEventReceived.getTagDefinition().getDescription(), createdTagDefinition.getDescription());
+ Assert.assertEquals(tagDefinitionSecondEventReceived.getBusEventType(), BusInternalEvent.BusInternalEventType.USER_TAGDEFINITION_DELETION);
+ Assert.assertEquals(tagDefinitionSecondEventReceived.getUserToken(), internalCallContext.getUserToken());
+ */
+ }
+
+ private static final class EventsListener {
+
+ private final List<BusInternalEvent> events = new ArrayList<BusInternalEvent>();
+ private final List<TagDefinitionInternalEvent> tagDefinitionEvents = new ArrayList<TagDefinitionInternalEvent>();
+
+ @Subscribe
+ public synchronized void processEvent(final BusInternalEvent event) {
+ events.add(event);
+ }
+
+ @Subscribe
+ public synchronized void processTagDefinitionEvent(final TagDefinitionInternalEvent event) {
+ tagDefinitionEvents.add(event);
+ }
+
+ public List<BusInternalEvent> getEvents() {
+ return events;
+ }
+
+ public List<TagDefinitionInternalEvent> getTagDefinitionEvents() {
+ return tagDefinitionEvents;
+ }
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/tag/TestTagStore.java b/util/src/test/java/org/killbill/billing/util/tag/TestTagStore.java
new file mode 100644
index 0000000..6ff114f
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/tag/TestTagStore.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.tag;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.annotations.Test;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.api.TagApiException;
+import org.killbill.billing.util.api.TagDefinitionApiException;
+import org.killbill.billing.util.tag.dao.TagDefinitionModelDao;
+import org.killbill.billing.util.tag.dao.TagModelDao;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestTagStore extends UtilTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testTagCreationAndRetrieval() throws TagApiException, TagDefinitionApiException {
+ final UUID accountId = UUID.randomUUID();
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ tagDefinitionDao.create("tag1", "First tag", internalCallContext);
+ assertListenerStatus();
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ final TagDefinitionModelDao testTagDefinition = tagDefinitionDao.create("testTagDefinition", "Second tag", internalCallContext);
+ assertListenerStatus();
+
+ final Tag tag = new DescriptiveTag(testTagDefinition.getId(), ObjectType.ACCOUNT, accountId, clock.getUTCNow());
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG);
+ tagDao.create(new TagModelDao(tag), internalCallContext);
+ assertListenerStatus();
+
+ final TagModelDao savedTag = tagDao.getById(tag.getId(), internalCallContext);
+ assertEquals(savedTag.getTagDefinitionId(), tag.getTagDefinitionId());
+ assertEquals(savedTag.getId(), tag.getId());
+ }
+
+ @Test(groups = "slow")
+ public void testControlTagCreation() throws TagApiException {
+ final UUID accountId = UUID.randomUUID();
+
+ final ControlTag tag = new DefaultControlTag(ControlTagType.AUTO_INVOICING_OFF, ObjectType.ACCOUNT, accountId, clock.getUTCNow());
+ eventsListener.pushExpectedEvent(NextEvent.TAG);
+ tagDao.create(new TagModelDao(tag), internalCallContext);
+ assertListenerStatus();
+
+ final TagModelDao savedTag = tagDao.getById(tag.getId(), internalCallContext);
+ assertEquals(savedTag.getTagDefinitionId(), tag.getTagDefinitionId());
+ assertEquals(savedTag.getId(), tag.getId());
+ }
+
+ @Test(groups = "slow", expectedExceptions = TagDefinitionApiException.class)
+ public void testTagDefinitionCreationWithControlTagName() throws TagDefinitionApiException {
+ final String definitionName = ControlTagType.AUTO_PAY_OFF.toString();
+ tagDefinitionDao.create(definitionName, "This should break", internalCallContext);
+ }
+
+ @Test(groups = "slow")
+ public void testTagDefinitionDeletionForUnusedDefinition() throws TagDefinitionApiException {
+ final String definitionName = "TestTag1234";
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ tagDefinitionDao.create(definitionName, "Some test tag", internalCallContext);
+ assertListenerStatus();
+
+ TagDefinitionModelDao tagDefinition = tagDefinitionDao.getByName(definitionName, internalCallContext);
+ assertNotNull(tagDefinition);
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ tagDefinitionDao.deleteById(tagDefinition.getId(), internalCallContext);
+ assertListenerStatus();
+
+ tagDefinition = tagDefinitionDao.getByName(definitionName, internalCallContext);
+ assertNull(tagDefinition);
+ }
+
+ @Test(groups = "slow", expectedExceptions = TagDefinitionApiException.class)
+ public void testTagDefinitionDeletionForDefinitionInUse() throws TagDefinitionApiException, TagApiException {
+ final String definitionName = "TestTag12345";
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ tagDefinitionDao.create(definitionName, "Some test tag", internalCallContext);
+ assertListenerStatus();
+
+ final TagDefinitionModelDao tagDefinition = tagDefinitionDao.getByName(definitionName, internalCallContext);
+ assertNotNull(tagDefinition);
+
+ final UUID objectId = UUID.randomUUID();
+ final Tag tag = new DescriptiveTag(tagDefinition.getId(), ObjectType.ACCOUNT, objectId, internalCallContext.getCreatedDate());
+ eventsListener.pushExpectedEvent(NextEvent.TAG);
+ tagDao.create(new TagModelDao(tag), internalCallContext);
+ assertListenerStatus();
+
+ tagDefinitionDao.deleteById(tagDefinition.getId(), internalCallContext);
+ }
+
+ @Test(groups = "slow")
+ public void testDeleteTagBeforeDeleteTagDefinition() throws TagDefinitionApiException, TagApiException {
+ final String definitionName = "TestTag1234567";
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ tagDefinitionDao.create(definitionName, "Some test tag", internalCallContext);
+ assertListenerStatus();
+
+ final TagDefinitionModelDao tagDefinition = tagDefinitionDao.getByName(definitionName, internalCallContext);
+ assertNotNull(tagDefinition);
+
+ final UUID objectId = UUID.randomUUID();
+
+ final Tag tag = new DescriptiveTag(tagDefinition.getId(), ObjectType.ACCOUNT, objectId, internalCallContext.getCreatedDate());
+ eventsListener.pushExpectedEvent(NextEvent.TAG);
+ tagDao.create(new TagModelDao(tag), internalCallContext);
+ assertListenerStatus();
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG);
+ tagDao.deleteTag(objectId, ObjectType.ACCOUNT, tagDefinition.getId(), internalCallContext);
+ assertListenerStatus();
+
+ eventsListener.pushExpectedEvent(NextEvent.TAG_DEFINITION);
+ tagDefinitionDao.deleteById(tagDefinition.getId(), internalCallContext);
+ assertListenerStatus();
+ }
+
+ @Test(groups = "slow")
+ public void testGetTagDefinitions() {
+ final List<TagDefinitionModelDao> definitionList = tagDefinitionDao.getTagDefinitions(internalCallContext);
+ assertTrue(definitionList.size() >= ControlTagType.values().length);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/template/translation/TestDefaultTranslatorBase.java b/util/src/test/java/org/killbill/billing/util/template/translation/TestDefaultTranslatorBase.java
new file mode 100644
index 0000000..eff1e43
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/template/translation/TestDefaultTranslatorBase.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.template.translation;
+
+import java.util.Locale;
+import java.util.UUID;
+
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+
+public class TestDefaultTranslatorBase extends UtilTestSuiteNoDB {
+
+ private final class TestTranslatorBase extends DefaultTranslatorBase {
+
+ public TestTranslatorBase(final TranslatorConfig config) {
+ super(config);
+ }
+
+ @Override
+ protected String getBundlePath() {
+ return UUID.randomUUID().toString();
+ }
+
+ @Override
+ protected String getTranslationType() {
+ return UUID.randomUUID().toString();
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testResourceDoesNotExist() throws Exception {
+ final TestTranslatorBase translator = new TestTranslatorBase(Mockito.mock(TranslatorConfig.class));
+ final String originalText = UUID.randomUUID().toString();
+ Assert.assertEquals(translator.getTranslation(Locale.FRANCE, originalText), originalText);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/timezone/TestDateAndTimeZoneContext.java b/util/src/test/java/org/killbill/billing/util/timezone/TestDateAndTimeZoneContext.java
new file mode 100644
index 0000000..f7e50a0
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/timezone/TestDateAndTimeZoneContext.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.timezone;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+//
+// There are two categories of tests, one that test the offset calculation and one that calculates
+// how to get a DateTime from a LocalDate (in account time zone)
+//
+// Tests {1, 2, 3} use an account timezone with a negative offset (-8) and tests {A, B, C} use an account timezone with a positive offset (+8)
+//
+public class TestDateAndTimeZoneContext extends UtilTestSuiteNoDB {
+
+ private final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTimeParser();
+
+ final String effectiveDateTime1 = "2012-01-20T07:30:42.000Z";
+ final String effectiveDateTime2 = "2012-01-20T08:00:00.000Z";
+ final String effectiveDateTime3 = "2012-01-20T08:45:33.000Z";
+
+ final String effectiveDateTimeA = "2012-01-20T16:30:42.000Z";
+ final String effectiveDateTimeB = "2012-01-20T16:00:00.000Z";
+ final String effectiveDateTimeC = "2012-01-20T15:30:42.000Z";
+
+
+ //
+ // Take an negative timezone offset and a reference time that is less than the offset (07:30:42 < 8)
+ // => to expect a negative offset of one day
+ //
+ @Test(groups = "fast")
+ public void testComputeOffset1() {
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime1);
+
+ int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+ assertEquals(offset, -1);
+ }
+
+ //
+ // Take an negative timezone offset and a reference time that is equal than the offset (08:00:00 = 8)
+ // => to expect an offset of 0
+ //
+ @Test(groups = "fast")
+ public void testComputeOffset2() {
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime2);
+
+ int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+ assertEquals(offset, 0);
+ }
+
+ //
+ // Take an negative timezone offset and a reference time that is greater than the offset (08:45:33 > 8)
+ // => to expect an offset of 0
+ //
+ @Test(groups = "fast")
+ public void testComputeOffset3() {
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime3);
+
+ int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+ assertEquals(offset, 0);
+ }
+
+ //
+ // Take an positive timezone offset and a reference time that closer to the end of the day than the timezone (16:30:42 + 8 > 24)
+ // => to expect a positive offset of one day
+ //
+ @Test(groups = "fast")
+ public void testComputeOffsetA() {
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeA);
+
+ int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+ assertEquals(offset, 1);
+ }
+
+ //
+ // Take an positive timezone offset and a reference time that brings us exactly at the end of the day (16:00:00 + 8 = 24)
+ // => to expect an offset of 1
+ //
+ @Test(groups = "fast")
+ public void testComputeOffsetB() {
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeB);
+
+ int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+ assertEquals(offset, 1);
+ }
+
+ //
+ // Take an positive timezone offset and a reference time that further away to the end of the day (15:30:42 + 8 < 24)
+ // => to expect an offset of 0
+ //
+ @Test(groups = "fast")
+ public void testComputeOffsetC() {
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeC);
+
+ int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+ assertEquals(offset, 0);
+ }
+
+ @Test(groups = "fast")
+ public void testComputeUTCDateTimeFromLocalDate1() {
+
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime1);
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+ final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+ final LocalDate endDate = new LocalDate(2013, 01, 19);
+ final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+ assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+ }
+
+
+ @Test(groups = "fast")
+ public void testComputeUTCDateTimeFromLocalDate2() {
+
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime2);
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+ final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+ final LocalDate endDate = new LocalDate(2013, 01, 20);
+ final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+ assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+ }
+
+
+ @Test(groups = "fast")
+ public void testComputeUTCDateTimeFromLocalDate3() {
+
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime3);
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+ final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+ final LocalDate endDate = new LocalDate(2013, 01, 20);
+ final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+ assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+ }
+
+ @Test(groups = "fast")
+ public void testComputeUTCDateTimeFromLocalDateA() {
+
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeA);
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+ final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+ final LocalDate endDate = new LocalDate(2013, 01, 21);
+ final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+ assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+ }
+
+ @Test(groups = "fast")
+ public void testComputeUTCDateTimeFromLocalDateB() {
+
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeB);
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+ final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+ final LocalDate endDate = new LocalDate(2013, 01, 21);
+ final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+ assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+ }
+
+ @Test(groups = "fast")
+ public void testComputeUTCDateTimeFromLocalDateC() {
+
+ final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeC);
+
+ final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+ final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+ final LocalDate endDate = new LocalDate(2013, 01, 20);
+ final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+ assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/UtilTestSuiteNoDB.java b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteNoDB.java
new file mode 100644
index 0000000..bb960a0
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteNoDB.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util;
+
+import javax.inject.Inject;
+
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.config.Ini;
+import org.apache.shiro.config.IniSecurityManagerFactory;
+import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.Factory;
+import org.apache.shiro.util.ThreadContext;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.billing.security.Permission;
+import org.killbill.billing.security.api.SecurityApi;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.audit.dao.AuditDao;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.glue.TestUtilModuleNoDB;
+import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+
+public class UtilTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
+
+ @Inject
+ protected PersistentBus eventBus;
+ @Inject
+ protected CacheControllerDispatcher controlCacheDispatcher;
+ @Inject
+ protected NonEntityDao nonEntityDao;
+ @Inject
+ protected InternalCallContextFactory internalCallContextFactory;
+ @Inject
+ protected CacheControllerDispatcher cacheControllerDispatcher;
+ @Inject
+ protected AuditDao auditDao;
+ @Inject
+ protected AuditUserApi auditUserApi;
+ @Inject
+ protected SecurityApi securityApi;
+ @Inject
+ protected KillBillJndiLdapRealm killBillJndiLdapRealm;
+
+ @BeforeClass(groups = "fast")
+ public void beforeClass() throws Exception {
+ final Injector g = Guice.createInjector(Stage.PRODUCTION, new TestUtilModuleNoDB(configSource));
+ g.injectMembers(this);
+ }
+
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() throws Exception {
+ eventBus.start();
+ }
+
+ @AfterMethod(groups = "fast")
+ public void afterMethod() throws Exception {
+ eventBus.stop();
+ }
+
+ // Security helpers
+
+ protected void login(final String username) {
+ logout();
+
+ final AuthenticationToken token = new UsernamePasswordToken(username, "password");
+ final Subject currentUser = SecurityUtils.getSubject();
+ currentUser.login(token);
+ }
+
+ protected void logout() {
+ final Subject currentUser = SecurityUtils.getSubject();
+ if (currentUser.isAuthenticated()) {
+ currentUser.logout();
+ }
+ }
+
+ protected void configureShiro() {
+ final Ini config = new Ini();
+ config.addSection("users");
+ config.getSection("users").put("pierre", "password, creditor");
+ config.getSection("users").put("stephane", "password, refunder");
+ config.addSection("roles");
+ config.getSection("roles").put("creditor", Permission.INVOICE_CAN_CREDIT.toString() + "," + Permission.INVOICE_CAN_ITEM_ADJUST.toString());
+ config.getSection("roles").put("refunder", Permission.PAYMENT_CAN_REFUND.toString());
+
+ // Reset the security manager
+ ThreadContext.unbindSecurityManager();
+
+ final Factory<SecurityManager> factory = new IniSecurityManagerFactory(config);
+ final SecurityManager securityManager = factory.getInstance();
+ SecurityUtils.setSecurityManager(securityManager);
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..b29dcd3
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util;
+
+import javax.inject.Inject;
+
+import org.skife.jdbi.v2.IDBI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+import org.killbill.billing.api.TestApiListener;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.notificationq.api.NotificationQueueService;
+import org.killbill.billing.util.audit.dao.AuditDao;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.customfield.api.DefaultCustomFieldUserApi;
+import org.killbill.billing.util.customfield.dao.CustomFieldDao;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.export.dao.DatabaseExportDao;
+import org.killbill.billing.util.glue.TestUtilModuleWithEmbeddedDB;
+import org.killbill.billing.util.tag.dao.DefaultTagDao;
+import org.killbill.billing.util.tag.dao.TagDefinitionDao;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+
+public abstract class UtilTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
+
+ private static final Logger log = LoggerFactory.getLogger(UtilTestSuiteWithEmbeddedDB.class);
+
+ @Inject
+ protected PersistentBus eventBus;
+ @Inject
+ protected CacheControllerDispatcher controlCacheDispatcher;
+ @Inject
+ protected NonEntityDao nonEntityDao;
+ @Inject
+ protected InternalCallContextFactory internalCallContextFactory;
+ @Inject
+ protected DefaultCustomFieldUserApi customFieldUserApi;
+ @Inject
+ protected CustomFieldDao customFieldDao;
+ @Inject
+ protected DatabaseExportDao dao;
+ @Inject
+ protected NotificationQueueService queueService;
+ @Inject
+ protected TagDefinitionDao tagDefinitionDao;
+ @Inject
+ protected DefaultTagDao tagDao;
+ @Inject
+ protected AuditDao auditDao;
+ @Inject
+ protected GlobalLocker locker;
+ @Inject
+ protected IDBI idbi;
+ @Inject
+ protected TestApiListener eventsListener;
+
+ @BeforeClass(groups = "slow")
+ public void beforeClass() throws Exception {
+ final Injector g = Guice.createInjector(Stage.PRODUCTION, new TestUtilModuleWithEmbeddedDB(configSource));
+ g.injectMembers(this);
+ }
+
+ @Override
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ eventsListener.reset();
+
+ eventBus.start();
+ eventBus.register(eventsListener);
+
+ controlCacheDispatcher.clearAll();
+
+ // Make sure we start with a clean state
+ assertListenerStatus();
+ }
+
+ @AfterMethod(groups = "slow")
+ public void afterMethod() throws Exception {
+ // Make sure we finish in a clean state
+ assertListenerStatus();
+
+ eventBus.unregister(eventsListener);
+ eventBus.stop();
+ }
+
+ protected void assertListenerStatus() {
+ eventsListener.assertListenerStatus();
+ }
+}
diff --git a/util/src/test/java/org/killbill/billing/util/validation/TestValidationManager.java b/util/src/test/java/org/killbill/billing/util/validation/TestValidationManager.java
new file mode 100644
index 0000000..064a22b
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/validation/TestValidationManager.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.validation;
+
+import java.util.Collection;
+
+import org.joda.time.DateTime;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
+import org.killbill.billing.util.validation.dao.DatabaseSchemaDao;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+public class TestValidationManager extends UtilTestSuiteWithEmbeddedDB {
+
+ private static final String TABLE_NAME = "validation_test";
+
+ private ValidationManager vm;
+
+ @Override
+ @BeforeClass(groups = "slow")
+ public void beforeClass() throws Exception {
+ super.beforeClass();
+ final DatabaseSchemaDao dao = new DatabaseSchemaDao(dbi);
+ vm = new ValidationManager(dao);
+ vm.loadSchemaInformation(helper.getDatabaseName());
+ }
+
+
+ @Test(groups = "slow")
+ public void testRetrievingColumnInfo() {
+ final Collection<DefaultColumnInfo> columnInfoList = vm.getTableInfo(TABLE_NAME);
+ assertEquals(columnInfoList.size(), 4);
+ assertNotNull(vm.getColumnInfo(TABLE_NAME, "column1"));
+ assertNull(vm.getColumnInfo(TABLE_NAME, "bogus"));
+
+ final DefaultColumnInfo numericColumnInfo = vm.getColumnInfo(TABLE_NAME, "column3");
+ assertNotNull(numericColumnInfo);
+ assertEquals(numericColumnInfo.getScale(), 4);
+ assertEquals(numericColumnInfo.getPrecision(), 10);
+ }
+
+ @Test(groups = "slow")
+ public void testSimpleConfiguration() {
+ final String STRING_FIELD_2 = "column2";
+ final String STRING_FIELD_2_PROPERTY = "stringField2";
+
+ final SimpleTestClass testObject = new SimpleTestClass(null, null, 7.9, new DateTime());
+
+ vm.setConfiguration(testObject.getClass(), STRING_FIELD_2_PROPERTY, vm.getColumnInfo(TABLE_NAME, STRING_FIELD_2));
+
+ assertTrue(vm.hasConfiguration(testObject.getClass()));
+ assertFalse(vm.hasConfiguration(ValidationManager.class));
+
+ final ValidationConfiguration configuration = vm.getConfiguration(SimpleTestClass.class);
+ assertNotNull(configuration);
+ assertTrue(configuration.hasMapping(STRING_FIELD_2_PROPERTY));
+
+ // set char field to value that is too short
+ assertFalse(vm.validate(testObject));
+ testObject.setStringField2("a");
+ assertFalse(vm.validate(testObject));
+
+ // set char to excessively long string
+ testObject.setStringField2("abc");
+ assertFalse(vm.validate(testObject));
+
+ // set char to proper length
+ testObject.setStringField2("ab");
+ assertTrue(vm.validate(testObject));
+
+ // add the first string field and add a string that exceeds the length
+ final String STRING_FIELD_1 = "column1";
+ final String STRING_FIELD_1_PROPERTY = "stringField1";
+ vm.setConfiguration(testObject.getClass(), STRING_FIELD_1_PROPERTY, vm.getColumnInfo(TABLE_NAME, STRING_FIELD_1));
+
+ assertTrue(vm.validate(testObject));
+ testObject.setStringField1("This is a long string that exceeds the length limit for column 1.");
+ assertFalse(vm.validate(testObject));
+ testObject.setStringField1("This is a short string.");
+ assertTrue(vm.validate(testObject));
+
+ // verify numeric values
+ final String NUMERIC_FIELD = "column3";
+ final String NUMERIC_FIELD_PROPERTY = "numericField1";
+ vm.setConfiguration(testObject.getClass(), NUMERIC_FIELD_PROPERTY, vm.getColumnInfo(TABLE_NAME, NUMERIC_FIELD));
+ assertTrue(vm.validate(testObject));
+
+ // set the value to have more than 4 decimal places
+ testObject.setNumericField1(0.123456);
+ assertFalse(vm.validate(testObject));
+
+ // set the value to have more than 10 digits
+ testObject.setNumericField1(12345678901234D);
+ assertFalse(vm.validate(testObject));
+
+ // set to a valid value
+ testObject.setNumericField1(1234567890);
+ assertTrue(vm.validate(testObject));
+
+ // check another valid number
+ testObject.setNumericField1(123456.7891);
+ assertTrue(vm.validate(testObject));
+
+ // check another valid number
+ testObject.setNumericField1(12345678.91);
+ assertTrue(vm.validate(testObject));
+
+
+ }
+
+ private class SimpleTestClass {
+
+ private String stringField1;
+ private String stringField2;
+ private double numericField1;
+ private DateTime dateTimeField1;
+
+ public SimpleTestClass(final String stringField1, final String stringField2, final double numericField1, final DateTime dateTimeField1) {
+ this.stringField1 = stringField1;
+ this.stringField2 = stringField2;
+ this.numericField1 = numericField1;
+ this.dateTimeField1 = dateTimeField1;
+ }
+
+ public String getStringField1() {
+ return stringField1;
+ }
+
+ public void setStringField1(final String stringField1) {
+ this.stringField1 = stringField1;
+ }
+
+ public String getStringField2() {
+ return stringField2;
+ }
+
+ public void setStringField2(final String stringField2) {
+ this.stringField2 = stringField2;
+ }
+
+ public double getNumericField1() {
+ return numericField1;
+ }
+
+ public void setNumericField1(final double numericField1) {
+ this.numericField1 = numericField1;
+ }
+
+ public DateTime getDateTimeField1() {
+ return dateTimeField1;
+ }
+
+ public void setDateTimeField1(final DateTime dateTimeField1) {
+ this.dateTimeField1 = dateTimeField1;
+ }
+ }
+}
diff --git a/util/src/test/resources/org/killbill/billing/util/dao/Kombucha.sql.stg b/util/src/test/resources/org/killbill/billing/util/dao/Kombucha.sql.stg
new file mode 100644
index 0000000..4fe6ff0
--- /dev/null
+++ b/util/src/test/resources/org/killbill/billing/util/dao/Kombucha.sql.stg
@@ -0,0 +1,21 @@
+group Kombucha: EntitySqlDao;
+
+tableName() ::= "kombucha"
+
+historyTableName() ::= "kombucha_history"
+
+tableFields(prefix) ::= <<
+ <prefix>tea
+, <prefix>mushroom
+, <prefix>sugar
+>>
+
+tableValues() ::= <<
+ :tea
+, :mushroom
+, :sugar
+>>
+
+isIsTimeForKombucha() ::= <<
+select hour(current_timestamp()) = 17 as is_time;
+>>
\ No newline at end of file
diff --git a/util/src/test/resources/org/killbill/billing/util/ddl_test.sql b/util/src/test/resources/org/killbill/billing/util/ddl_test.sql
new file mode 100644
index 0000000..c4c8894
--- /dev/null
+++ b/util/src/test/resources/org/killbill/billing/util/ddl_test.sql
@@ -0,0 +1,35 @@
+/*! SET storage_engine=INNODB */;
+
+DROP TABLE IF EXISTS dummy;
+CREATE TABLE dummy (
+ dummy_id char(36) NOT NULL,
+ value varchar(256) NOT NULL,
+ PRIMARY KEY(dummy_id)
+);
+
+DROP TABLE IF EXISTS dummy2;
+CREATE TABLE dummy2 (
+ id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ dummy_id char(36) NOT NULL,
+ PRIMARY KEY(id)
+);
+
+DROP TABLE IF EXISTS validation_test;
+CREATE TABLE validation_test (
+ column1 varchar(25),
+ column2 char(2) NOT NULL,
+ column3 numeric(10,4),
+ column4 datetime
+);
+
+DROP TABLE IF EXISTS kombucha;
+CREATE TABLE kombucha (
+ record_id int(11) unsigned NOT NULL AUTO_INCREMENT,
+ id char(36) NOT NULL,
+ tea varchar(50) NOT NULL,
+ mushroom varchar(50) NOT NULL,
+ sugar varchar(50) NOT NULL,
+ account_record_id int(11) unsigned default null,
+ tenant_record_id int(11) unsigned default null,
+ PRIMARY KEY(record_id)
+);
diff --git a/util/src/test/resources/org/killbill/billing/util/email/templates/HtmlInvoiceTemplate.mustache b/util/src/test/resources/org/killbill/billing/util/email/templates/HtmlInvoiceTemplate.mustache
new file mode 100644
index 0000000..be1668b
--- /dev/null
+++ b/util/src/test/resources/org/killbill/billing/util/email/templates/HtmlInvoiceTemplate.mustache
@@ -0,0 +1,96 @@
+<html>
+ <head>
+ <style type="text/css">
+ th {align=left; width=225px; border-bottom: solid 2px black;}
+ </style>
+ </head>
+ <body>
+ <h1>{{text.invoiceTitle}}</h1>
+ <table>
+ <tr>
+ <td rowspan=3 width=350px>Insert image here</td>
+ <td width=100px/>
+ <td width=225px/>
+ <td width=225px/>
+ </tr>
+ <tr>
+ <td />
+ <td align=right>{{text.invoiceDate}}</td>
+ <td>{{invoice.formattedInvoiceDate}}</td>
+ </tr>
+ <tr>
+ <td />
+ <td align=right>{{text.invoiceNumber}}</td>
+ <td>{{invoice.invoiceNumber}}</td>
+ </tr>
+ <tr>
+ <td>{{text.companyName}}</td>
+ <td></td>
+ <td align=right>{{text.accountOwnerName}}</td>
+ <td>{{account.name}}</td>
+ </tr>
+ <tr>
+ <td>{{text.companyAddress}}</td>
+ <td />
+ <td />
+ <td>{{account.email}}</td>
+ </tr>
+ <tr>
+ <td>{{text.companyCityProvincePostalCode}}</td>
+ <td />
+ <td />
+ <td>{{account.phone}}</td>
+ </tr>
+ <tr>
+ <td>{{text.companyCountry}}</td>
+ <td />
+ <td />
+ <td />
+ </tr>
+ <tr>
+ <td><{{text.companyUrl}}</td>
+ <td />
+ <td />
+ <td />
+ </tr>
+ </table>
+ <br />
+ <br />
+ <br />
+ <table>
+ <tr>
+ <th>{{text.invoiceItemBundleName}}</td>
+ <th>{{text.invoiceItemDescription}}</td>
+ <th>{{text.invoiceItemServicePeriod}}</td>
+ <th>{{text.invoiceItemAmount}}</td>
+ </tr>
+ {{#invoice.invoiceItems}}
+ <tr>
+ <td>{{description}}</td>
+ <td>{{planName}}</td>
+ <td>{{formattedStartDate}} - {{formattedEndDate}}</td>
+ <td>{{invoice.currency}} {{amount}}</td>
+ </tr>
+ {{/invoice.invoiceItems}}
+ <tr>
+ <td colspan=4 />
+ </tr>
+ <tr>
+ <td colspan=2 />
+ <td align=right><strong>{{text.invoiceAmount}}</strong></td>
+ <td align=right><strong>{{invoice.chargedAmount}}</strong></td>
+ </tr>
+ <tr>
+ <td colspan=2 />
+ <td align=right><strong>{{text.invoiceAmountPaid}}</strong></td>
+ <td align=right><strong>{{invoice.paidAmount}}</strong></td>
+ </tr>
+ <tr>
+ <td colspan=2 />
+ <td align=right><strong>{{text.invoiceBalance}}</strong></td>
+ <td align=right><strong>{{invoice.balance}}</strong></td>
+ </tr>
+ </table>
+ </body>
+</html>
+
diff --git a/util/src/test/resources/org/killbill/billing/util/template/translation/CatalogTranslation_en_US.properties b/util/src/test/resources/org/killbill/billing/util/template/translation/CatalogTranslation_en_US.properties
new file mode 100644
index 0000000..96a4ff4
--- /dev/null
+++ b/util/src/test/resources/org/killbill/billing/util/template/translation/CatalogTranslation_en_US.properties
@@ -0,0 +1,2 @@
+shotgun-monthly = Monthly shotgun plan
+shotgun-annual = Annual shotgun plan
diff --git a/util/src/test/resources/org/killbill/billing/util/template/translation/CatalogTranslation_fr_CA.properties b/util/src/test/resources/org/killbill/billing/util/template/translation/CatalogTranslation_fr_CA.properties
new file mode 100644
index 0000000..d19e790
--- /dev/null
+++ b/util/src/test/resources/org/killbill/billing/util/template/translation/CatalogTranslation_fr_CA.properties
@@ -0,0 +1,2 @@
+shotgun-monthly = Fusil de chasse mensuel
+shotgun-annual = Fusil de chasse annuel