killbill-uncached

Merge branch 'master' into usage

3/9/2014 9:43:38 PM

Changes

.gitignore 4(+4 -0)

.idea/libraries/Maven__antlr_antlr_2_7_7.xml 13(+0 -13)

.idea/libraries/Maven__aopalliance_aopalliance_1_0.xml 13(+0 -13)

.idea/libraries/Maven__asm_asm_3_1.xml 13(+0 -13)

.idea/libraries/Maven__biz_aQute_bndlib_1_50_0.xml 13(+0 -13)

.idea/libraries/Maven__cglib_cglib_nodep_2_2.xml 13(+0 -13)

.idea/libraries/Maven__ch_qos_logback_logback_classic_1_0_1.xml 13(+0 -13)

.idea/libraries/Maven__ch_qos_logback_logback_core_1_0_1.xml 13(+0 -13)

.idea/libraries/Maven__classworlds_classworlds_1_1_alpha_2.xml 13(+0 -13)

.idea/libraries/Maven__com_beust_jcommander_1_12.xml 13(+0 -13)

.idea/libraries/Maven__com_codahale_metrics_metrics_core_3_0_1.xml 13(+0 -13)

.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_annotations_2_1_0.xml 13(+0 -13)

.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_core_2_1_0.xml 13(+0 -13)

.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_databind_2_1_0.xml 13(+0 -13)

.idea/libraries/Maven__com_fasterxml_jackson_dataformat_jackson_dataformat_csv_2_1_0.xml 13(+0 -13)

.idea/libraries/Maven__com_fasterxml_jackson_dataformat_jackson_dataformat_smile_2_0_1.xml 13(+0 -13)

.idea/libraries/Maven__com_fasterxml_jackson_datatype_jackson_datatype_joda_2_0_1.xml 13(+0 -13)

.idea/libraries/Maven__com_fasterxml_jackson_jaxrs_jackson_jaxrs_json_provider_2_0_0.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__com_google_code_findbugs_jsr305_1_3_9.xml 13(+0 -13)

.idea/libraries/Maven__com_google_inject_extensions_guice_multibindings_3_0.xml 13(+0 -13)

.idea/libraries/Maven__com_google_inject_extensions_guice_servlet_3_0.xml 13(+0 -13)

.idea/libraries/Maven__com_google_inject_guice_3_0.xml 13(+0 -13)

.idea/libraries/Maven__com_h2database_h2_1_3_158.xml 13(+0 -13)

.idea/libraries/Maven__com_jayway_awaitility_awaitility_1_3_3.xml 13(+0 -13)

.idea/libraries/Maven__com_mchange_c3p0_0_9_2.xml 13(+0 -13)

.idea/libraries/Maven__com_mchange_mchange_commons_java_0_2_3_3.xml 13(+0 -13)

.idea/libraries/Maven__com_mogwee_mogwee_executors_1_2_0.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_arecibo_arecibo_jmx_1_0_3.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_arecibo_arecibo_metrics_2_0_0.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_jaxrs_jaxrs_metrics_2_0_0.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_jetty_ning_service_skeleton_base_0_1_7.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_jetty_ning_service_skeleton_core_0_1_7.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_jetty_ning_service_skeleton_eventtracker_0_1_7.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_jetty_ning_service_skeleton_jdbi_0_1_7.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_jetty_ning_service_skeleton_log4j_0_1_7.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_jetty_ning_service_skeleton_utils_0_1_7.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_metrics_eventtracker_common_4_1_2.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_metrics_eventtracker_http_4_1_2.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_metrics_eventtracker_smile_4_1_2.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_metrics_serialization_common_2_2_0.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_metrics_serialization_smile_2_2_0.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_metrics_serialization_writer_2_2_0.xml 13(+0 -13)

.idea/libraries/Maven__com_samskivert_jmustache_1_5.xml 13(+0 -13)

.idea/libraries/Maven__com_sun_jersey_contribs_jersey_guice_1_12.xml 13(+0 -13)

.idea/libraries/Maven__com_sun_jersey_jersey_core_1_12.xml 13(+0 -13)

.idea/libraries/Maven__com_sun_jersey_jersey_server_1_12.xml 13(+0 -13)

.idea/libraries/Maven__com_sun_jersey_jersey_servlet_1_12.xml 13(+0 -13)

.idea/libraries/Maven__com_yammer_metrics_metrics_annotation_2_1_2.xml 13(+0 -13)

.idea/libraries/Maven__com_yammer_metrics_metrics_core_2_1_2.xml 13(+0 -13)

.idea/libraries/Maven__com_yammer_metrics_metrics_guice_2_1_2.xml 13(+0 -13)

.idea/libraries/Maven__com_yammer_metrics_metrics_jdbi_2_1_2.xml 13(+0 -13)

.idea/libraries/Maven__com_yammer_metrics_metrics_servlet_2_1_2.xml 13(+0 -13)

.idea/libraries/Maven__commons_beanutils_commons_beanutils_1_8_3.xml 13(+0 -13)

.idea/libraries/Maven__commons_io_commons_io_2_1.xml 13(+0 -13)

.idea/libraries/Maven__commons_lang_commons_lang_2_6.xml 13(+0 -13)

.idea/libraries/Maven__javax_activation_activation_1_1.xml 13(+0 -13)

.idea/libraries/Maven__javax_inject_javax_inject_1.xml 13(+0 -13)

.idea/libraries/Maven__javax_mail_mail_1_4_1.xml 13(+0 -13)

.idea/libraries/Maven__javax_servlet_javax_servlet_api_3_0_1.xml 13(+0 -13)

.idea/libraries/Maven__javax_ws_rs_jsr311_api_1_1_1.xml 13(+0 -13)

.idea/libraries/Maven__junit_junit_3_8_1.xml 13(+0 -13)

.idea/libraries/Maven__mysql_mysql_connector_java_5_1_22.xml 13(+0 -13)

.idea/libraries/Maven__mysql_mysql_connector_mxj_5_0_12.xml 13(+0 -13)

.idea/libraries/Maven__mysql_mysql_connector_mxj_db_files_5_0_12.xml 13(+0 -13)

.idea/libraries/Maven__net_sf_ehcache_ehcache_core_2_6_2.xml 13(+0 -13)

.idea/libraries/Maven__net_sf_kxml_kxml2_2_2_2.xml 13(+0 -13)

.idea/libraries/Maven__org_antlr_stringtemplate_3_2_1.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_commons_commons_compress_1_5.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_commons_commons_email_1_2.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_bundlerepository_1_6_6.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_dependencymanager_3_1_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_apache_felix_org_apache_felix_deploymentadmin_0_9_0.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_framework_4_0_3.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_gogo_runtime_0_10_0.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_log_1_0_1.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_metatype_1_0_6.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_scr_1_6_2.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_scr_annotations_1_7_0.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_scr_ds_annotations_1_2_0.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_shell_1_4_3.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_shell_remote_1_1_2.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_apache_felix_webconsole_3_1_8.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_felix_org_osgi_core_1_0_1.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_maven_maven_artifact_2_0_7.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_maven_maven_artifact_manager_2_0_7.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_maven_maven_model_2_0_7.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_maven_maven_plugin_api_2_0.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_maven_maven_plugin_registry_2_0_7.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_maven_maven_profile_2_0_7.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_maven_maven_project_2_0_7.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_maven_maven_repository_metadata_2_0_7.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_maven_maven_settings_2_0_7.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_maven_wagon_wagon_provider_api_1_0_beta_2.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_shiro_shiro_core_1_2_2.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_shiro_shiro_ehcache_1_2_2.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_shiro_shiro_guice_1_2_2.xml 13(+0 -13)

.idea/libraries/Maven__org_apache_shiro_shiro_web_1_2_2.xml 13(+0 -13)

.idea/libraries/Maven__org_beanshell_bsh_2_0b4.xml 13(+0 -13)

.idea/libraries/Maven__org_codehaus_jackson_jackson_core_asl_1_9_5.xml 13(+0 -13)

.idea/libraries/Maven__org_codehaus_jackson_jackson_mapper_asl_1_9_5.xml 13(+0 -13)

.idea/libraries/Maven__org_codehaus_plexus_plexus_container_default_1_0_alpha_9_stable_1.xml 13(+0 -13)

.idea/libraries/Maven__org_codehaus_plexus_plexus_utils_1_4_1.xml 13(+0 -13)

.idea/libraries/Maven__org_easymock_easymock_2_4.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_continuation_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_deploy_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_http_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_io_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_jmx_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_security_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_server_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_servlet_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_util_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_webapp_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_jetty_xml_8_1_11_v20130520.xml 13(+0 -13)

.idea/libraries/Maven__org_eclipse_jetty_orbit_javax_servlet_3_0_0_v201112011016.xml 13(+0 -13)

.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_1.xml 13(+0 -13)

.idea/libraries/Maven__org_hamcrest_hamcrest_library_1_1.xml 13(+0 -13)

.idea/libraries/Maven__org_json_json_20070829.xml 13(+0 -13)

.idea/libraries/Maven__org_mockito_mockito_all_1_9_0.xml 13(+0 -13)

.idea/libraries/Maven__org_objenesis_objenesis_1_2.xml 13(+0 -13)

.idea/libraries/Maven__org_osgi_org_osgi_compendium_4_3_1.xml 13(+0 -13)

.idea/libraries/Maven__org_osgi_org_osgi_core_4_3_1.xml 13(+0 -13)

.idea/libraries/Maven__org_skife_config_config_magic_0_14.xml 13(+0 -13)

.idea/libraries/Maven__org_slf4j_jcl_over_slf4j_1_7_5.xml 13(+0 -13)

.idea/libraries/Maven__org_slf4j_jul_to_slf4j_1_7_5.xml 13(+0 -13)

.idea/libraries/Maven__org_slf4j_osgi_over_slf4j_1_7_5.xml 13(+0 -13)

.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_5.xml 13(+0 -13)

.idea/libraries/Maven__org_slf4j_slf4j_simple_1_7_5.xml 13(+0 -13)

.idea/libraries/Maven__org_testng_testng_6_3_1.xml 13(+0 -13)

.idea/libraries/Maven__org_tukaani_xz_1_2.xml 13(+0 -13)

.idea/libraries/Maven__org_weakref_jmxutils_1_12.xml 13(+0 -13)

.idea/libraries/Maven__org_yaml_snakeyaml_1_6.xml 13(+0 -13)

.idea/libraries/Maven__xmlpull_xmlpull_1_1_3_1.xml 13(+0 -13)

.idea/vcs.xml 2(+1 -1)

account/pom.xml 70(+25 -45)

api/pom.xml 28(+14 -14)

api/src/main/java/com/ning/billing/events/TagInternalEvent.java 33(+0 -33)

api/src/main/java/com/ning/billing/events/UserTagCreationInternalEvent.java 21(+0 -21)

api/src/main/java/com/ning/billing/events/UserTagDefinitionCreationInternalEvent.java 21(+0 -21)

api/src/main/java/com/ning/billing/events/UserTagDefinitionDeletionInternalEvent.java 21(+0 -21)

api/src/main/java/com/ning/billing/events/UserTagDeletionInternalEvent.java 21(+0 -21)

api/src/main/java/com/ning/billing/glue/AccountModule.java 26(+0 -26)

api/src/main/java/com/ning/billing/glue/EntitlementModule.java 32(+0 -32)

api/src/main/java/com/ning/billing/glue/InvoiceModule.java 28(+0 -28)

api/src/main/java/com/ning/billing/glue/JunctionModule.java 22(+0 -22)

api/src/main/java/com/ning/billing/glue/OverdueModule.java 23(+0 -23)

api/src/main/java/com/ning/billing/glue/SubscriptionModule.java 30(+0 -30)

api/src/main/java/com/ning/billing/invoice/api/formatters/InvoiceFormatterFactory.java 27(+0 -27)

api/src/main/java/com/ning/billing/invoice/api/InvoiceInternalApi.java 74(+0 -74)

api/src/main/java/com/ning/billing/invoice/api/InvoiceMigrationApi.java 42(+0 -42)

api/src/main/java/com/ning/billing/invoice/api/InvoiceNotifier.java 25(+0 -25)

api/src/main/java/com/ning/billing/invoice/api/InvoiceService.java 23(+0 -23)

api/src/main/java/com/ning/billing/junction/BillingEvent.java 110(+0 -110)

api/src/main/java/com/ning/billing/junction/BillingEventSet.java 28(+0 -28)

api/src/main/java/com/ning/billing/junction/BillingInternalApi.java 29(+0 -29)

api/src/main/java/com/ning/billing/junction/BillingModeType.java 21(+0 -21)

api/src/main/java/com/ning/billing/junction/BlockingInternalApi.java 34(+0 -34)

api/src/main/java/com/ning/billing/lifecycle/KillbillService.java 56(+0 -56)

api/src/main/java/com/ning/billing/lifecycle/LifecycleHandlerType.java 114(+0 -114)

api/src/main/java/com/ning/billing/osgi/api/LiveTrackerException.java 28(+0 -28)

api/src/main/java/com/ning/billing/osgi/api/OSGIService.java 22(+0 -22)

api/src/main/java/com/ning/billing/osgi/api/OSGIServiceDescriptor.java 32(+0 -32)

api/src/main/java/com/ning/billing/osgi/api/OSGIServiceRegistration.java 51(+0 -51)

api/src/main/java/com/ning/billing/osgi/api/OSGIUserApi.java 20(+0 -20)

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/overdue/Condition.java 35(+0 -35)

api/src/main/java/com/ning/billing/overdue/config/api/BillingState.java 87(+0 -87)

api/src/main/java/com/ning/billing/overdue/config/api/OverdueException.java 42(+0 -42)

api/src/main/java/com/ning/billing/overdue/config/api/OverdueStateSet.java 46(+0 -46)

api/src/main/java/com/ning/billing/overdue/config/api/PaymentResponse.java 92(+0 -92)

api/src/main/java/com/ning/billing/overdue/EmailNotification.java 26(+0 -26)

api/src/main/java/com/ning/billing/overdue/OverdueApiException.java 38(+0 -38)

api/src/main/java/com/ning/billing/overdue/OverdueCancellationPolicy.java 23(+0 -23)

api/src/main/java/com/ning/billing/overdue/OverdueService.java 28(+0 -28)

api/src/main/java/com/ning/billing/overdue/OverdueState.java 43(+0 -43)

api/src/main/java/com/ning/billing/overdue/OverdueUserApi.java 35(+0 -35)

api/src/main/java/com/ning/billing/payment/api/PaymentInternalApi.java 41(+0 -41)

api/src/main/java/com/ning/billing/payment/api/PaymentService.java 27(+0 -27)

api/src/main/java/com/ning/billing/payment/plugin/api/NoOpPaymentPluginApi.java 28(+0 -28)

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/SubscriptionBase.java 105(+0 -105)

api/src/main/java/com/ning/billing/subscription/api/SubscriptionBaseService.java 25(+0 -25)

api/src/main/java/com/ning/billing/subscription/api/SubscriptionBaseTransitionType.java 67(+0 -67)

api/src/main/java/com/ning/billing/subscription/api/SubscriptionBillingApiException.java 36(+0 -36)

api/src/main/java/com/ning/billing/subscription/api/timeline/BundleBaseTimeline.java 49(+0 -49)

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/com/ning/billing/subscription/api/user/SubscriptionBaseBundle.java 34(+0 -34)

api/src/main/java/com/ning/billing/subscription/api/user/SubscriptionBaseTransition.java 68(+0 -68)

api/src/main/java/com/ning/billing/tag/TagInternalApi.java 46(+0 -46)

api/src/main/java/com/ning/billing/tenant/api/TenantService.java 23(+0 -23)

api/src/main/java/com/ning/billing/util/email/EmailApiException.java 36(+0 -36)

api/src/main/java/com/ning/billing/util/email/EmailSender.java 27(+0 -27)

api/src/main/java/com/ning/billing/util/template/translation/Translator.java 23(+0 -23)

api/src/main/java/com/ning/billing/util/template/translation/TranslatorConfig.java 61(+0 -61)

beatrix/pom.xml 126(+55 -71)

beatrix/src/main/java/com/ning/billing/beatrix/DefaultBeatrixService.java 76(+0 -76)

beatrix/src/main/java/com/ning/billing/beatrix/glue/BeatrixModule.java 65(+0 -65)

beatrix/src/main/java/com/ning/billing/beatrix/lifecycle/Lifecycle.java 28(+0 -28)

beatrix/src/main/resources/com/ning/billing/beatrix/ddl.sql 37(+0 -37)

beatrix/src/test/java/com/ning/billing/beatrix/BeatrixTestSuite.java 22(+0 -22)

beatrix/src/test/java/com/ning/billing/beatrix/BeatrixTestSuiteWithEmbeddedDB.java 22(+0 -22)

beatrix/src/test/java/com/ning/billing/beatrix/extbus/TestEventJson.java 49(+0 -49)

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/osgi/TestOSGIBase.java 23(+0 -23)

beatrix/src/test/java/com/ning/billing/beatrix/integration/osgi/TestOSGIIntegration.java 27(+0 -27)

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/com/ning/billing/beatrix/integration/overdue/TestOverdueBase.java 88(+0 -88)

beatrix/src/test/java/com/ning/billing/beatrix/integration/TestPublicBus.java 111(+0 -111)

beatrix/src/test/java/com/ning/billing/beatrix/util/AccountChecker.java 66(+0 -66)

beatrix/src/test/java/com/ning/billing/beatrix/util/PaymentChecker.java 115(+0 -115)

beatrix/src/test/java/com/ning/billing/beatrix/util/RefundChecker.java 134(+0 -134)

beatrix/src/test/java/com/ning/billing/beatrix/util/SubscriptionChecker.java 82(+0 -82)

bin/start-server 4(+2 -2)

catalog/pom.xml 28(+14 -14)

catalog/src/main/java/com/ning/billing/catalog/api/user/DefaultCatalogUserApi.java 41(+0 -41)

catalog/src/main/java/com/ning/billing/catalog/CreateCatalogSchema.java 41(+0 -41)

catalog/src/main/java/com/ning/billing/catalog/DefaultCatalogService.java 82(+0 -82)

catalog/src/main/java/com/ning/billing/catalog/DefaultDuration.java 117(+0 -117)

catalog/src/main/java/com/ning/billing/catalog/DefaultInternationalPrice.java 134(+0 -134)

catalog/src/main/java/com/ning/billing/catalog/DefaultLimit.java 91(+0 -91)

catalog/src/main/java/com/ning/billing/catalog/DefaultListing.java 43(+0 -43)

catalog/src/main/java/com/ning/billing/catalog/DefaultPrice.java 82(+0 -82)

catalog/src/main/java/com/ning/billing/catalog/DefaultPriceList.java 130(+0 -130)

catalog/src/main/java/com/ning/billing/catalog/DefaultPriceListSet.java 115(+0 -115)

catalog/src/main/java/com/ning/billing/catalog/DefaultUnit.java 48(+0 -48)

catalog/src/main/java/com/ning/billing/catalog/glue/CatalogModule.java 60(+0 -60)

catalog/src/main/java/com/ning/billing/catalog/io/ICatalogLoader.java 27(+0 -27)

catalog/src/main/java/com/ning/billing/catalog/LoadCatalog.java 39(+0 -39)

catalog/src/main/java/com/ning/billing/catalog/PriceListDefault.java 52(+0 -52)

catalog/src/main/java/com/ning/billing/catalog/rules/Case.java 81(+0 -81)

catalog/src/main/java/com/ning/billing/catalog/rules/CaseBillingAlignment.java 39(+0 -39)

catalog/src/main/java/com/ning/billing/catalog/rules/CaseCancelPolicy.java 38(+0 -38)

catalog/src/main/java/com/ning/billing/catalog/rules/CaseChange.java 154(+0 -154)

catalog/src/main/java/com/ning/billing/catalog/rules/CaseChangePlanAlignment.java 38(+0 -38)

catalog/src/main/java/com/ning/billing/catalog/rules/CaseChangePlanPolicy.java 40(+0 -40)

catalog/src/main/java/com/ning/billing/catalog/rules/CaseCreateAlignment.java 38(+0 -38)

catalog/src/main/java/com/ning/billing/catalog/rules/CasePhase.java 66(+0 -66)

catalog/src/main/java/com/ning/billing/catalog/rules/CasePriceList.java 93(+0 -93)

catalog/src/main/java/com/ning/billing/catalog/rules/CaseStandardNaming.java 77(+0 -77)

catalog/src/test/java/com/ning/billing/catalog/CatalogTestSuiteNoDB.java 39(+0 -39)

catalog/src/test/java/com/ning/billing/catalog/glue/TestCatalogModule.java 34(+0 -34)

catalog/src/test/java/com/ning/billing/catalog/glue/TestCatalogModuleNoDB.java 26(+0 -26)

catalog/src/test/java/com/ning/billing/catalog/io/TestVersionedCatalogLoader.java 128(+0 -128)

catalog/src/test/java/com/ning/billing/catalog/io/TestXMLReader.java 38(+0 -38)

catalog/src/test/java/com/ning/billing/catalog/MockCatalogModule.java 37(+0 -37)

catalog/src/test/java/com/ning/billing/catalog/MockCatalogService.java 56(+0 -56)

catalog/src/test/java/com/ning/billing/catalog/MockInternationalPrice.java 41(+0 -41)

catalog/src/test/java/com/ning/billing/catalog/MockPlan.java 135(+0 -135)

catalog/src/test/java/com/ning/billing/catalog/MockPlanPhase.java 122(+0 -122)

catalog/src/test/java/com/ning/billing/catalog/MockPriceList.java 28(+0 -28)

catalog/src/test/java/com/ning/billing/catalog/MockProduct.java 75(+0 -75)

catalog/src/test/java/com/ning/billing/catalog/rules/Result.java 21(+0 -21)

catalog/src/test/java/com/ning/billing/catalog/rules/TestLoadRules.java 55(+0 -55)

catalog/src/test/java/com/ning/billing/catalog/rules/TestPlanRules.java 122(+0 -122)

catalog/src/test/java/com/ning/billing/catalog/TestCatalogService.java 56(+0 -56)

catalog/src/test/java/com/ning/billing/catalog/TestInternationalPrice.java 89(+0 -89)

catalog/src/test/java/com/ning/billing/catalog/TestLimits.java 92(+0 -92)

catalog/src/test/java/com/ning/billing/catalog/TestPlan.java 54(+0 -54)

catalog/src/test/java/com/ning/billing/catalog/TestPlanPhase.java 74(+0 -74)

catalog/src/test/java/com/ning/billing/catalog/TestPriceListSet.java 87(+0 -87)

catalog/src/test/java/com/ning/billing/catalog/TestStandaloneCatalog.java 48(+0 -48)

catalog/src/test/java/com/ning/billing/catalog/TestVersionedCatalog.java 107(+0 -107)

catalog/src/test/java/com/ning/billing/catalog/util/CreateCatalogSchema.java 40(+0 -40)

currency/pom.xml 52(+16 -36)

currency/src/main/java/com/ning/billing/currency/api/DefaultCurrencyConversion.java 44(+0 -44)

currency/src/main/java/com/ning/billing/currency/api/DefaultCurrencyConversionApi.java 73(+0 -73)

currency/src/main/java/com/ning/billing/currency/DefaultCurrencyProviderPluginRegistry.java 69(+0 -69)

currency/src/main/java/com/ning/billing/currency/DefaultCurrencyService.java 34(+0 -34)

currency/src/main/java/com/ning/billing/currency/glue/CurrencyModule.java 54(+0 -54)

currency/src/main/java/com/ning/billing/currency/glue/DefaultCurrencyProviderPluginRegistryProvider.java 39(+0 -39)

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/DefaultSubscription.java 81(+0 -81)

entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionBundle.java 86(+0 -86)

entitlement/src/main/java/com/ning/billing/entitlement/api/EntitlementDateHelper.java 116(+0 -116)

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/block/BlockingChecker.java 57(+0 -57)

entitlement/src/main/java/com/ning/billing/entitlement/dao/BlockingStateDao.java 78(+0 -78)

entitlement/src/main/java/com/ning/billing/entitlement/dao/BlockingStateModelDao.java 175(+0 -175)

entitlement/src/main/java/com/ning/billing/entitlement/dao/BlockingStateSqlDao.java 102(+0 -102)

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/resources/com/ning/billing/entitlement/dao/BlockingStateSqlDao.sql.stg 102(+0 -102)

entitlement/src/main/resources/com/ning/billing/entitlement/ddl.sql 25(+0 -25)

entitlement/src/test/java/com/ning/billing/entitlement/api/TestEntitlementDateHelper.java 141(+0 -141)

entitlement/src/test/java/com/ning/billing/entitlement/api/TestEventJson.java 43(+0 -43)

entitlement/src/test/java/com/ning/billing/entitlement/block/MockBlockingChecker.java 51(+0 -51)

entitlement/src/test/java/com/ning/billing/entitlement/block/TestBlockingApi.java 113(+0 -113)

entitlement/src/test/java/com/ning/billing/entitlement/dao/MockBlockingStateDao.java 106(+0 -106)

entitlement/src/test/java/com/ning/billing/entitlement/dao/TestBlockingDao.java 98(+0 -98)

entitlement/src/test/java/com/ning/billing/entitlement/EntitlementTestListenerStatus.java 59(+0 -59)

entitlement/src/test/java/com/ning/billing/entitlement/EntitlementTestSuiteNoDB.java 75(+0 -75)

entitlement/src/test/java/com/ning/billing/entitlement/glue/TestEntitlementModule.java 39(+0 -39)

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)

invoice/pom.xml 80(+30 -50)

invoice/src/main/java/com/ning/billing/invoice/api/DefaultInvoiceService.java 77(+0 -77)

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/migration/MigrationInvoice.java 31(+0 -31)

invoice/src/main/java/com/ning/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java 148(+0 -148)

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/api/user/DefaultNullInvoiceEvent.java 111(+0 -111)

invoice/src/main/java/com/ning/billing/invoice/calculator/InvoiceCalculatorUtils.java 194(+0 -194)

invoice/src/main/java/com/ning/billing/invoice/dao/CBADao.java 122(+0 -122)

invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java 171(+0 -171)

invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceItemModelDao.java 294(+0 -294)

invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceItemSqlDao.java 40(+0 -40)

invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceModelDao.java 227(+0 -227)

invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceModelDaoHelper.java 62(+0 -62)

invoice/src/main/java/com/ning/billing/invoice/dao/InvoicePaymentModelDao.java 219(+0 -219)

invoice/src/main/java/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.java 78(+0 -78)

invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceSqlDao.java 53(+0 -53)

invoice/src/main/java/com/ning/billing/invoice/generator/BillingIntervalDetail.java 156(+0 -156)

invoice/src/main/java/com/ning/billing/invoice/generator/DefaultInvoiceGenerator.java 259(+0 -259)

invoice/src/main/java/com/ning/billing/invoice/generator/InvoiceDateUtils.java 192(+0 -192)

invoice/src/main/java/com/ning/billing/invoice/generator/InvoiceGenerator.java 35(+0 -35)

invoice/src/main/java/com/ning/billing/invoice/glue/DefaultInvoiceModule.java 139(+0 -139)

invoice/src/main/java/com/ning/billing/invoice/InvoiceListener.java 130(+0 -130)

invoice/src/main/java/com/ning/billing/invoice/InvoiceTagHandler.java 71(+0 -71)

invoice/src/main/java/com/ning/billing/invoice/model/AdjInvoiceItem.java 46(+0 -46)

invoice/src/main/java/com/ning/billing/invoice/model/BillingMode.java 32(+0 -32)

invoice/src/main/java/com/ning/billing/invoice/model/CreditAdjInvoiceItem.java 51(+0 -51)

invoice/src/main/java/com/ning/billing/invoice/model/CreditBalanceAdjInvoiceItem.java 58(+0 -58)

invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoice.java 223(+0 -223)

invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoicePayment.java 123(+0 -123)

invoice/src/main/java/com/ning/billing/invoice/model/ExternalChargeInvoiceItem.java 61(+0 -61)

invoice/src/main/java/com/ning/billing/invoice/model/FixedPriceInvoiceItem.java 62(+0 -62)

invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java 129(+0 -129)

invoice/src/main/java/com/ning/billing/invoice/model/InvalidDateSequenceException.java 21(+0 -21)

invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemBase.java 263(+0 -263)

invoice/src/main/java/com/ning/billing/invoice/model/InvoiceItemFactory.java 87(+0 -87)

invoice/src/main/java/com/ning/billing/invoice/model/ItemAdjInvoiceItem.java 58(+0 -58)

invoice/src/main/java/com/ning/billing/invoice/model/MigrationInvoiceItem.java 34(+0 -34)

invoice/src/main/java/com/ning/billing/invoice/model/RecurringInvoiceItem.java 59(+0 -59)

invoice/src/main/java/com/ning/billing/invoice/model/RecurringInvoiceItemData.java 89(+0 -89)

invoice/src/main/java/com/ning/billing/invoice/model/RefundAdjInvoiceItem.java 51(+0 -51)

invoice/src/main/java/com/ning/billing/invoice/model/RepairAdjInvoiceItem.java 51(+0 -51)

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/EmailInvoiceNotifier.java 106(+0 -106)

invoice/src/main/java/com/ning/billing/invoice/notification/NextBillingDateNotificationKey.java 32(+0 -32)

invoice/src/main/java/com/ning/billing/invoice/notification/NextBillingDateNotifier.java 30(+0 -30)

invoice/src/main/java/com/ning/billing/invoice/notification/NextBillingDatePoster.java 33(+0 -33)

invoice/src/main/java/com/ning/billing/invoice/notification/NullInvoiceNotifier.java 30(+0 -30)

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/HtmlInvoiceGenerator.java 76(+0 -76)

invoice/src/main/java/com/ning/billing/invoice/template/translator/DefaultInvoiceTranslator.java 148(+0 -148)

invoice/src/main/java/com/ning/billing/invoice/template/translator/InvoiceStrings.java 61(+0 -61)

invoice/src/main/java/com/ning/billing/invoice/tree/AccountItemTree.java 189(+0 -189)

invoice/src/main/java/com/ning/billing/invoice/tree/Item.java 193(+0 -193)

invoice/src/main/java/com/ning/billing/invoice/tree/ItemsInterval.java 194(+0 -194)

invoice/src/main/java/com/ning/billing/invoice/tree/ItemsNodeInterval.java 265(+0 -265)

invoice/src/main/java/com/ning/billing/invoice/tree/NodeInterval.java 398(+0 -398)

invoice/src/main/java/com/ning/billing/invoice/tree/SubscriptionItemTree.java 281(+0 -281)

invoice/src/main/resources/com/ning/billing/invoice/dao/InvoiceItemSqlDao.sql.stg 56(+0 -56)

invoice/src/main/resources/com/ning/billing/invoice/dao/InvoicePaymentSqlDao.sql.stg 101(+0 -101)

invoice/src/main/resources/com/ning/billing/invoice/dao/InvoiceSqlDao.sql.stg 63(+0 -63)

invoice/src/main/resources/com/ning/billing/invoice/ddl.sql 74(+0 -74)

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/api/MockInvoicePaymentApi.java 155(+0 -155)

invoice/src/test/java/com/ning/billing/invoice/api/user/TestEventJson.java 53(+0 -53)

invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java 331(+0 -331)

invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDaoUnit.java 72(+0 -72)

invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDaoForItemAdjustment.java 168(+0 -168)

invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceItemDao.java 236(+0 -236)

invoice/src/test/java/com/ning/billing/invoice/generator/TestBillingIntervalDetail.java 168(+0 -168)

invoice/src/test/java/com/ning/billing/invoice/generator/TestInvoiceDateUtils.java 124(+0 -124)

invoice/src/test/java/com/ning/billing/invoice/glue/TestInvoiceModule.java 66(+0 -66)

invoice/src/test/java/com/ning/billing/invoice/glue/TestInvoiceModuleNoDB.java 99(+0 -99)

invoice/src/test/java/com/ning/billing/invoice/glue/TestInvoiceModuleWithEmbeddedDb.java 54(+0 -54)

invoice/src/test/java/com/ning/billing/invoice/InvoiceTestSuiteNoDB.java 117(+0 -117)

invoice/src/test/java/com/ning/billing/invoice/InvoiceTestSuiteWithEmbeddedDB.java 149(+0 -149)

invoice/src/test/java/com/ning/billing/invoice/MockBillingEventSet.java 59(+0 -59)

invoice/src/test/java/com/ning/billing/invoice/model/TestExternalChargeInvoiceItem.java 70(+0 -70)

invoice/src/test/java/com/ning/billing/invoice/model/TestInAdvanceBillingMode.java 164(+0 -164)

invoice/src/test/java/com/ning/billing/invoice/model/TestItemAdjInvoiceItem.java 40(+0 -40)

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/TestHtmlInvoiceGenerator.java 129(+0 -129)

invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java 222(+0 -222)

invoice/src/test/java/com/ning/billing/invoice/TestInvoiceNotificationQListener.java 53(+0 -53)

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/TestProRation.java 69(+0 -69)

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/com/ning/billing/invoice/tests/InternationalPriceMock.java 52(+0 -52)

invoice/src/test/java/com/ning/billing/invoice/tests/InvoiceTestUtils.java 121(+0 -121)

invoice/src/test/java/com/ning/billing/invoice/tests/ProRationTestBase.java 88(+0 -88)

invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java 151(+0 -151)

invoice/src/test/java/com/ning/billing/invoice/tree/TestNodeInterval.java 312(+0 -312)

invoice/src/test/resources/com/ning/billing/util/template/translation/InvoiceTranslation_en_US.properties 22(+0 -22)

jaxrs/pom.xml 40(+20 -20)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountEmailJson.java 114(+0 -114)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountJson.java 470(+0 -470)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java 208(+0 -208)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/AuditLogJson.java 136(+0 -136)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/BillingExceptionJson.java 250(+0 -250)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJson.java 135(+0 -135)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleTimelineJson.java 109(+0 -109)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/CatalogJsonSimple.java 415(+0 -415)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/ChargebackJson.java 161(+0 -161)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java 141(+0 -141)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/CustomFieldJson.java 128(+0 -128)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceEmailJson.java 81(+0 -81)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceItemJson.java 246(+0 -246)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJson.java 256(+0 -256)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/JsonBase.java 62(+0 -62)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/NotificationJson.java 67(+0 -67)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/OverdueStateJson.java 165(+0 -165)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJson.java 292(+0 -292)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentMethodJson.java 592(+0 -592)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/PlanDetailJson.java 160(+0 -160)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java 250(+0 -250)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/SessionJson.java 126(+0 -126)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/SubjectJson.java 115(+0 -115)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/SubscriptionJson.java 522(+0 -522)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/TagDefinitionJson.java 155(+0 -155)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/TagJson.java 117(+0 -117)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/TenantJson.java 129(+0 -129)

jaxrs/src/main/java/com/ning/billing/jaxrs/json/TenantKeyJson.java 44(+0 -44)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/AccountApiExceptionMapper.java 63(+0 -63)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/BillingExceptionBaseMapper.java 42(+0 -42)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/BlockingApiExceptionMapper.java 42(+0 -42)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/CatalogApiExceptionMapper.java 42(+0 -42)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/CurrencyValueNullMapper.java 42(+0 -42)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/EmailApiExceptionMapper.java 42(+0 -42)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/EntitlementApiExceptionMapper.java 51(+0 -51)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/EntityPersistenceExceptionMapper.java 42(+0 -42)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java 186(+0 -186)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/IllegalArgumentExceptionMapper.java 41(+0 -41)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/IllegalPlanChangeMapper.java 42(+0 -42)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/InvoiceApiExceptionMapper.java 75(+0 -75)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/OverdueApiExceptionMapper.java 42(+0 -42)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/OverdueErrorMapper.java 42(+0 -42)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/PaymentApiExceptionMapper.java 105(+0 -105)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/RuntimeExceptionMapper.java 56(+0 -56)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ShiroExceptionMapper.java 42(+0 -42)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/SubscriptionApiExceptionMapper.java 89(+0 -89)

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/com/ning/billing/jaxrs/mappers/TagApiExceptionMapper.java 49(+0 -49)

jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/TagDefinitionApiExceptionMapper.java 53(+0 -53)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AuditMode.java 68(+0 -68)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/BundleResource.java 318(+0 -318)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/CatalogResource.java 134(+0 -134)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/ChargebackResource.java 111(+0 -111)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/CreditResource.java 122(+0 -122)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/CustomFieldResource.java 114(+0 -114)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/ExportResource.java 83(+0 -83)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java 180(+0 -180)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java 311(+0 -311)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentMethodResource.java 224(+0 -224)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java 391(+0 -391)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PluginResource.java 268(+0 -268)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/RefundResource.java 195(+0 -195)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SecurityResource.java 87(+0 -87)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java 477(+0 -477)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TagDefinitionResource.java 136(+0 -136)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TagResource.java 131(+0 -131)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TenantResource.java 152(+0 -152)

jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TestResource.java 218(+0 -218)

jaxrs/src/main/java/com/ning/billing/jaxrs/util/Context.java 78(+0 -78)

jaxrs/src/main/java/com/ning/billing/jaxrs/util/JaxrsUriBuilder.java 75(+0 -75)

jaxrs/src/main/java/com/ning/billing/jaxrs/util/KillbillEventHandler.java 72(+0 -72)

jaxrs/src/test/java/com/ning/billing/jaxrs/glue/TestJaxrsModule.java 33(+0 -33)

jaxrs/src/test/java/com/ning/billing/jaxrs/glue/TestJaxrsModuleNoDB.java 28(+0 -28)

jaxrs/src/test/java/com/ning/billing/jaxrs/JaxrsTestSuiteNoDB.java 39(+0 -39)

jaxrs/src/test/java/com/ning/billing/jaxrs/JaxrsTestUtils.java 38(+0 -38)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAccountEmailJson.java 60(+0 -60)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAccountJson.java 129(+0 -129)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAccountTimelineJson.java 22(+0 -22)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAuditLogJson.java 82(+0 -82)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBillingExceptionJson.java 71(+0 -71)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java 60(+0 -60)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleTimelineJson.java 101(+0 -101)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackJson.java 57(+0 -57)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java 54(+0 -54)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCustomFieldJson.java 48(+0 -48)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestEntitlementJsonWithEvents.java 70(+0 -70)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceEmailJson.java 45(+0 -45)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceItemJsonSimple.java 111(+0 -111)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java 115(+0 -115)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestOverdueStateJson.java 52(+0 -52)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestPlanDetailJason.java 89(+0 -89)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestRefundJson.java 87(+0 -87)

jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestTagDefinitionJson.java 48(+0 -48)

jaxrs/src/test/java/com/ning/billing/jaxrs/TestDateConversion.java 75(+0 -75)

junction/pom.xml 76(+28 -48)

junction/src/main/java/com/ning/billing/junction/glue/DefaultJunctionModule.java 50(+0 -50)

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/test/java/com/ning/billing/junction/glue/TestJunctionModule.java 64(+0 -64)

junction/src/test/java/com/ning/billing/junction/glue/TestJunctionModuleNoDB.java 66(+0 -66)

junction/src/test/java/com/ning/billing/junction/glue/TestJunctionModuleWithEmbeddedDB.java 59(+0 -59)

junction/src/test/java/com/ning/billing/junction/JunctionTestListenerStatus.java 57(+0 -57)

junction/src/test/java/com/ning/billing/junction/JunctionTestSuiteNoDB.java 77(+0 -77)

junction/src/test/java/com/ning/billing/junction/JunctionTestSuiteWithEmbeddedDB.java 224(+0 -224)

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/TestBillingApi.java 286(+0 -286)

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)

NEWS 3(+3 -0)

osgi/pom.xml 64(+32 -32)

osgi/src/main/java/com/ning/billing/osgi/api/DefaultOSGIUserApi.java 21(+0 -21)

osgi/src/main/java/com/ning/billing/osgi/ContextClassLoaderHelper.java 103(+0 -103)

osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIKillbill.java 173(+0 -173)

osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIService.java 173(+0 -173)

osgi/src/main/java/com/ning/billing/osgi/DefaultOSGIServiceDescriptor.java 40(+0 -40)

osgi/src/main/java/com/ning/billing/osgi/FileInstall.java 225(+0 -225)

osgi/src/main/java/com/ning/billing/osgi/glue/DefaultOSGIModule.java 94(+0 -94)

osgi/src/main/java/com/ning/billing/osgi/glue/OSGIDataSourceConfig.java 57(+0 -57)

osgi/src/main/java/com/ning/billing/osgi/glue/OSGIDataSourceProvider.java 154(+0 -154)

osgi/src/main/java/com/ning/billing/osgi/http/DefaultHttpContext.java 45(+0 -45)

osgi/src/main/java/com/ning/billing/osgi/http/DefaultHttpService.java 75(+0 -75)

osgi/src/main/java/com/ning/billing/osgi/http/DefaultServletRouter.java 161(+0 -161)

osgi/src/main/java/com/ning/billing/osgi/http/OSGIServlet.java 132(+0 -132)

osgi/src/main/java/com/ning/billing/osgi/http/StaticServlet.java 86(+0 -86)

osgi/src/main/java/com/ning/billing/osgi/KillbillActivator.java 169(+0 -169)

osgi/src/main/java/com/ning/billing/osgi/KillbillEventObservable.java 63(+0 -63)

osgi/src/main/java/com/ning/billing/osgi/pluginconf/DefaultPluginConfig.java 117(+0 -117)

osgi/src/main/java/com/ning/billing/osgi/pluginconf/DefaultPluginConfigServiceApi.java 56(+0 -56)

osgi/src/main/java/com/ning/billing/osgi/pluginconf/DefaultPluginJavaConfig.java 64(+0 -64)

osgi/src/main/java/com/ning/billing/osgi/pluginconf/DefaultPluginRubyConfig.java 72(+0 -72)

osgi/src/main/java/com/ning/billing/osgi/pluginconf/PluginConfigException.java 30(+0 -30)

osgi/src/main/java/com/ning/billing/osgi/pluginconf/PluginFinder.java 211(+0 -211)

osgi/src/main/java/com/ning/billing/osgi/pluginconf/PuginConfServiceApi.java 21(+0 -21)

osgi/src/main/java/com/ning/billing/osgi/PureOSGIBundleFinder.java 72(+0 -72)

osgi/src/test/java/com/ning/billing/osgi/TestKillbillActivator.java 75(+0 -75)

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/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/meter/src/main/java/com/ning/billing/meter/jaxrs/resources/MeterResource.java 230(+0 -230)

osgi-bundles/bundles/meter/src/test/java/com/ning/billing/meter/MeterTestSuite.java 71(+0 -71)

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/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/tests/beatrix/src/main/java/com/ning/billing/osgi/bundles/test/Dummy.java 21(+0 -21)

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/payment/src/main/java/com/ning/billing/osgi/bundles/test/Dummy.java 21(+0 -21)

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)

overdue/pom.xml 72(+26 -46)

overdue/src/main/java/com/ning/billing/overdue/api/DefaultOverdueUserApi.java 99(+0 -99)

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/applicator/OverdueEmailGenerator.java 56(+0 -56)

overdue/src/main/java/com/ning/billing/overdue/applicator/OverdueStateApplicator.java 377(+0 -377)

overdue/src/main/java/com/ning/billing/overdue/calculator/BillingStateCalculator.java 108(+0 -108)

overdue/src/main/java/com/ning/billing/overdue/config/DefaultCondition.java 112(+0 -112)

overdue/src/main/java/com/ning/billing/overdue/config/DefaultDuration.java 112(+0 -112)

overdue/src/main/java/com/ning/billing/overdue/config/DefaultEmailNotification.java 51(+0 -51)

overdue/src/main/java/com/ning/billing/overdue/config/DefaultOverdueState.java 173(+0 -173)

overdue/src/main/java/com/ning/billing/overdue/config/DefaultOverdueStateSet.java 101(+0 -101)

overdue/src/main/java/com/ning/billing/overdue/config/OverdueConfig.java 55(+0 -55)

overdue/src/main/java/com/ning/billing/overdue/config/OverdueStatesAccount.java 56(+0 -56)

overdue/src/main/java/com/ning/billing/overdue/CreateOverdueConfigSchema.java 42(+0 -42)

overdue/src/main/java/com/ning/billing/overdue/exceptions/OverdueError.java 38(+0 -38)

overdue/src/main/java/com/ning/billing/overdue/glue/DefaultOverdueModule.java 89(+0 -89)

overdue/src/main/java/com/ning/billing/overdue/listener/OverdueDispatcher.java 64(+0 -64)

overdue/src/main/java/com/ning/billing/overdue/listener/OverdueListener.java 110(+0 -110)

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/OverdueAsyncBusNotifier.java 81(+0 -81)

overdue/src/main/java/com/ning/billing/overdue/notification/OverdueAsyncBusPoster.java 56(+0 -56)

overdue/src/main/java/com/ning/billing/overdue/notification/OverdueCheckNotificationKey.java 32(+0 -32)

overdue/src/main/java/com/ning/billing/overdue/notification/OverdueCheckNotifier.java 67(+0 -67)

overdue/src/main/java/com/ning/billing/overdue/notification/OverdueCheckPoster.java 78(+0 -78)

overdue/src/main/java/com/ning/billing/overdue/notification/OverdueNotifier.java 36(+0 -36)

overdue/src/main/java/com/ning/billing/overdue/notification/OverduePoster.java 30(+0 -30)

overdue/src/main/java/com/ning/billing/overdue/OverdueProperties.java 31(+0 -31)

overdue/src/main/java/com/ning/billing/overdue/service/DefaultOverdueService.java 147(+0 -147)

overdue/src/main/java/com/ning/billing/overdue/wrapper/OverdueWrapper.java 81(+0 -81)

overdue/src/main/java/com/ning/billing/overdue/wrapper/OverdueWrapperFactory.java 108(+0 -108)

overdue/src/test/java/com/ning/billing/overdue/applicator/formatters/TestDefaultBillingStateFormatter.java 40(+0 -40)

overdue/src/test/java/com/ning/billing/overdue/applicator/OverdueBusListenerTester.java 49(+0 -49)

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/config/io/TestReadConfig.java 33(+0 -33)

overdue/src/test/java/com/ning/billing/overdue/config/MockOverdueRules.java 30(+0 -30)

overdue/src/test/java/com/ning/billing/overdue/config/MockOverdueState.java 32(+0 -32)

overdue/src/test/java/com/ning/billing/overdue/config/TestCondition.java 173(+0 -173)

overdue/src/test/java/com/ning/billing/overdue/config/TestOverdueConfig.java 84(+0 -84)

overdue/src/test/java/com/ning/billing/overdue/glue/ApplicatorMockJunctionModule.java 70(+0 -70)

overdue/src/test/java/com/ning/billing/overdue/glue/TestOverdueModule.java 62(+0 -62)

overdue/src/test/java/com/ning/billing/overdue/glue/TestOverdueModuleNoDB.java 41(+0 -41)

overdue/src/test/java/com/ning/billing/overdue/glue/TestOverdueModuleWithEmbeddedDB.java 43(+0 -43)

overdue/src/test/java/com/ning/billing/overdue/notification/MockOverdueNotifier.java 45(+0 -45)

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/com/ning/billing/overdue/OverdueTestSuiteNoDB.java 110(+0 -110)

overdue/src/test/java/com/ning/billing/overdue/OverdueTestSuiteWithEmbeddedDB.java 120(+0 -120)

overdue/src/test/java/com/ning/billing/overdue/TestOverdueHelper.java 147(+0 -147)

overdue/src/test/java/com/ning/billing/overdue/wrapper/TestOverdueWrapper.java 82(+0 -82)

payment/pom.xml 63(+21 -42)

payment/src/main/java/com/ning/billing/payment/api/DefaultPayment.java 207(+0 -207)

payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java 282(+0 -282)

payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentErrorEvent.java 107(+0 -107)

payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentInfoEvent.java 210(+0 -210)

payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentMethod.java 75(+0 -75)

payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentPluginErrorEvent.java 106(+0 -106)

payment/src/main/java/com/ning/billing/payment/api/DefaultRefund.java 167(+0 -167)

payment/src/main/java/com/ning/billing/payment/api/svcs/DefaultPaymentInternalApi.java 68(+0 -68)

payment/src/main/java/com/ning/billing/payment/bus/InvoiceHandler.java 77(+0 -77)

payment/src/main/java/com/ning/billing/payment/bus/PaymentTagHandler.java 85(+0 -85)

payment/src/main/java/com/ning/billing/payment/core/PaymentMethodProcessor.java 460(+0 -460)

payment/src/main/java/com/ning/billing/payment/core/ProcessorBase.java 211(+0 -211)

payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java 504(+0 -504)

payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java 409(+0 -409)

payment/src/main/java/com/ning/billing/payment/dao/PaymentAttemptModelDao.java 252(+0 -252)

payment/src/main/java/com/ning/billing/payment/dao/PaymentAttemptSqlDao.java 48(+0 -48)

payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java 81(+0 -81)

payment/src/main/java/com/ning/billing/payment/dao/PaymentMethodModelDao.java 129(+0 -129)

payment/src/main/java/com/ning/billing/payment/dao/PaymentMethodSqlDao.java 67(+0 -67)

payment/src/main/java/com/ning/billing/payment/dao/PaymentModelDao.java 281(+0 -281)

payment/src/main/java/com/ning/billing/payment/dao/PaymentSqlDao.java 80(+0 -80)

payment/src/main/java/com/ning/billing/payment/dao/RefundModelDao.java 224(+0 -224)

payment/src/main/java/com/ning/billing/payment/dao/RefundSqlDao.java 65(+0 -65)

payment/src/main/java/com/ning/billing/payment/dispatcher/PluginDispatcher.java 70(+0 -70)

payment/src/main/java/com/ning/billing/payment/glue/DefaultPaymentProviderPluginRegistryProvider.java 59(+0 -59)

payment/src/main/java/com/ning/billing/payment/glue/DefaultPaymentService.java 109(+0 -109)

payment/src/main/java/com/ning/billing/payment/glue/PaymentModule.java 114(+0 -114)

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/com/ning/billing/payment/retry/AutoPayRetryService.java 74(+0 -74)

payment/src/main/java/com/ning/billing/payment/retry/BaseRetryService.java 160(+0 -160)

payment/src/main/java/com/ning/billing/payment/retry/FailedPaymentRetryService.java 108(+0 -108)

payment/src/main/java/com/ning/billing/payment/retry/PaymentRetryNotificationKey.java 31(+0 -31)

payment/src/main/java/com/ning/billing/payment/retry/PluginFailureRetryService.java 111(+0 -111)

payment/src/main/java/com/ning/billing/payment/retry/RetryService.java 38(+0 -38)

payment/src/main/resources/com/ning/billing/payment/dao/PaymentAttemptSqlDao.sql.stg 74(+0 -74)

payment/src/main/resources/com/ning/billing/payment/dao/PaymentMethodSqlDao.sql.stg 85(+0 -85)

payment/src/main/resources/com/ning/billing/payment/dao/PaymentSqlDao.sql.stg 116(+0 -116)

payment/src/main/resources/com/ning/billing/payment/dao/RefundSqlDao.sql.stg 77(+0 -77)

payment/src/main/resources/com/ning/billing/payment/ddl.sql 196(+0 -196)

payment/src/test/java/com/ning/billing/payment/api/TestEventJson.java 54(+0 -54)

payment/src/test/java/com/ning/billing/payment/api/TestPaymentApi.java 82(+0 -82)

payment/src/test/java/com/ning/billing/payment/api/TestPaymentApiNoDB.java 179(+0 -179)

payment/src/test/java/com/ning/billing/payment/api/TestPaymentMethodPlugin.java 55(+0 -55)

payment/src/test/java/com/ning/billing/payment/core/TestPaymentMethodProcessorNoDB.java 62(+0 -62)

payment/src/test/java/com/ning/billing/payment/core/TestPaymentMethodProcessorRefreshWithDB.java 110(+0 -110)

payment/src/test/java/com/ning/billing/payment/dao/MockPaymentDao.java 228(+0 -228)

payment/src/test/java/com/ning/billing/payment/dao/TestPaymentDao.java 287(+0 -287)

payment/src/test/java/com/ning/billing/payment/dispatcher/TestPluginDispatcher.java 93(+0 -93)

payment/src/test/java/com/ning/billing/payment/glue/TestPaymentModule.java 77(+0 -77)

payment/src/test/java/com/ning/billing/payment/glue/TestPaymentModuleNoDB.java 44(+0 -44)

payment/src/test/java/com/ning/billing/payment/glue/TestPaymentModuleWithEmbeddedDB.java 37(+0 -37)

payment/src/test/java/com/ning/billing/payment/MockInvoice.java 211(+0 -211)

payment/src/test/java/com/ning/billing/payment/MockInvoiceCreationEvent.java 153(+0 -153)

payment/src/test/java/com/ning/billing/payment/MockRecurringInvoiceItem.java 195(+0 -195)

payment/src/test/java/com/ning/billing/payment/PaymentTestSuiteNoDB.java 96(+0 -96)

payment/src/test/java/com/ning/billing/payment/PaymentTestSuiteWithEmbeddedDB.java 100(+0 -100)

payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java 228(+0 -228)

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/com/ning/billing/payment/TestPaymentHelper.java 132(+0 -132)

payment/src/test/java/com/ning/billing/payment/TestRetryService.java 230(+0 -230)

pom.xml 6(+3 -3)

server/pom.xml 238(+126 -112)

server/src/main/java/com/ning/billing/server/config/KillbillServerConfig.java 36(+0 -36)

server/src/main/java/com/ning/billing/server/config/UpdateCheckConfig.java 43(+0 -43)

server/src/main/java/com/ning/billing/server/DefaultServerService.java 70(+0 -70)

server/src/main/java/com/ning/billing/server/filters/KillbillGuiceFilter.java 44(+0 -44)

server/src/main/java/com/ning/billing/server/healthchecks/KillbillHealthcheck.java 41(+0 -41)

server/src/main/java/com/ning/billing/server/listeners/KillbillGuiceListener.java 154(+0 -154)

server/src/main/java/com/ning/billing/server/modules/DataSourceProvider.java 126(+0 -126)

server/src/main/java/com/ning/billing/server/modules/DBIProvider.java 98(+0 -98)

server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java 179(+0 -179)

server/src/main/java/com/ning/billing/server/modules/KillBillShiroWebModule.java 78(+0 -78)

server/src/main/java/com/ning/billing/server/notifications/PushNotificationListener.java 114(+0 -114)

server/src/main/java/com/ning/billing/server/security/KillbillJdbcRealm.java 88(+0 -88)

server/src/main/java/com/ning/billing/server/security/TenantFilter.java 142(+0 -142)

server/src/main/java/com/ning/billing/server/ServerService.java 22(+0 -22)

server/src/main/java/com/ning/billing/server/updatechecker/ClientInfo.java 159(+0 -159)

server/src/main/java/com/ning/billing/server/updatechecker/ProductInfo.java 106(+0 -106)

server/src/main/java/com/ning/billing/server/updatechecker/Tracker.java 92(+0 -92)

server/src/main/java/com/ning/billing/server/updatechecker/UpdateChecker.java 108(+0 -108)

server/src/main/java/com/ning/billing/server/updatechecker/UpdateListProperties.java 88(+0 -88)

server/src/main/resources/com/ning/billing/server/version.properties 6(+0 -6)

server/src/test/java/com/ning/billing/jaxrs/KillbillClient.java 173(+0 -173)

server/src/test/java/com/ning/billing/jaxrs/TestAccount.java 296(+0 -296)

server/src/test/java/com/ning/billing/jaxrs/TestAccountEmail.java 77(+0 -77)

server/src/test/java/com/ning/billing/jaxrs/TestAccountEmailNotifications.java 58(+0 -58)

server/src/test/java/com/ning/billing/jaxrs/TestAccountTimeline.java 289(+0 -289)

server/src/test/java/com/ning/billing/jaxrs/TestBundle.java 130(+0 -130)

server/src/test/java/com/ning/billing/jaxrs/TestCatalog.java 67(+0 -67)

server/src/test/java/com/ning/billing/jaxrs/TestChargeback.java 188(+0 -188)

server/src/test/java/com/ning/billing/jaxrs/TestCredit.java 91(+0 -91)

server/src/test/java/com/ning/billing/jaxrs/TestCustomField.java 84(+0 -84)

server/src/test/java/com/ning/billing/jaxrs/TestEntitlement.java 170(+0 -170)

server/src/test/java/com/ning/billing/jaxrs/TestExceptions.java 53(+0 -53)

server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java 481(+0 -481)

server/src/test/java/com/ning/billing/jaxrs/TestInvoiceNotification.java 57(+0 -57)

server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java 346(+0 -346)

server/src/test/java/com/ning/billing/jaxrs/TestJetty.java 71(+0 -71)

server/src/test/java/com/ning/billing/jaxrs/TestOverdue.java 75(+0 -75)

server/src/test/java/com/ning/billing/jaxrs/TestPayment.java 292(+0 -292)

server/src/test/java/com/ning/billing/jaxrs/TestPaymentMethod.java 88(+0 -88)

server/src/test/java/com/ning/billing/jaxrs/TestPlugin.java 222(+0 -222)

server/src/test/java/com/ning/billing/jaxrs/TestPushNotification.java 192(+0 -192)

server/src/test/java/com/ning/billing/jaxrs/TestSecurity.java 61(+0 -61)

server/src/test/java/com/ning/billing/jaxrs/TestTag.java 165(+0 -165)

server/src/test/java/com/ning/billing/server/security/TestKillbillJdbcRealm.java 101(+0 -101)

server/src/test/java/com/ning/billing/server/security/TestTenantFilter.java 84(+0 -84)

subscription/pom.xml 80(+30 -50)

subscription/src/main/java/com/ning/billing/subscription/alignment/BaseAligner.java 54(+0 -54)

subscription/src/main/java/com/ning/billing/subscription/alignment/MigrationPlanAligner.java 210(+0 -210)

subscription/src/main/java/com/ning/billing/subscription/alignment/PlanAligner.java 342(+0 -342)

subscription/src/main/java/com/ning/billing/subscription/alignment/TimedMigration.java 126(+0 -126)

subscription/src/main/java/com/ning/billing/subscription/alignment/TimedPhase.java 78(+0 -78)

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/SubscriptionApiBase.java 67(+0 -67)

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/addon/AddonUtils.java 122(+0 -122)

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/EventListener.java 27(+0 -27)

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/BundleSqlDao.java 74(+0 -74)

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/EventBase.java 163(+0 -163)

subscription/src/main/java/com/ning/billing/subscription/events/EventBaseBuilder.java 144(+0 -144)

subscription/src/main/java/com/ning/billing/subscription/events/phase/PhaseEvent.java 25(+0 -25)

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/ApiEvent.java 34(+0 -34)

subscription/src/main/java/com/ning/billing/subscription/events/user/ApiEventBase.java 87(+0 -87)

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/ApiEventCancel.java 25(+0 -25)

subscription/src/main/java/com/ning/billing/subscription/events/user/ApiEventChange.java 25(+0 -25)

subscription/src/main/java/com/ning/billing/subscription/events/user/ApiEventCreate.java 25(+0 -25)

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/ApiEventType.java 74(+0 -74)

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/resources/com/ning/billing/subscription/ddl.sql 72(+0 -72)

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/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/alignment/TestTimedPhase.java 41(+0 -41)

subscription/src/test/java/com/ning/billing/subscription/api/migration/TestMigration.java 302(+0 -302)

subscription/src/test/java/com/ning/billing/subscription/api/TestEventJson.java 61(+0 -61)

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)

tenant/pom.xml 66(+23 -43)

tenant/src/main/java/com/ning/billing/tenant/api/DefaultTenant.java 124(+0 -124)

tenant/src/main/java/com/ning/billing/tenant/api/DefaultTenantKV.java 45(+0 -45)

tenant/src/main/java/com/ning/billing/tenant/api/DefaultTenantService.java 27(+0 -27)

tenant/src/main/java/com/ning/billing/tenant/api/user/DefaultTenantUserApi.java 115(+0 -115)

tenant/src/main/java/com/ning/billing/tenant/dao/DefaultTenantDao.java 154(+0 -154)

tenant/src/main/java/com/ning/billing/tenant/dao/TenantDao.java 36(+0 -36)

tenant/src/main/java/com/ning/billing/tenant/dao/TenantKVModelDao.java 124(+0 -124)

tenant/src/main/java/com/ning/billing/tenant/dao/TenantKVSqlDao.java 45(+0 -45)

tenant/src/main/java/com/ning/billing/tenant/dao/TenantModelDao.java 144(+0 -144)

tenant/src/main/java/com/ning/billing/tenant/dao/TenantSqlDao.java 34(+0 -34)

tenant/src/main/java/com/ning/billing/tenant/glue/TenantModule.java 60(+0 -60)

tenant/src/main/java/com/ning/billing/tenant/security/KillbillCredentialsMatcher.java 42(+0 -42)

tenant/src/main/resources/com/ning/billing/tenant/dao/TenantKVSqlDao.sql.stg 48(+0 -48)

tenant/src/main/resources/com/ning/billing/tenant/dao/TenantSqlDao.sql.stg 66(+0 -66)

tenant/src/main/resources/com/ning/billing/tenant/ddl.sql 35(+0 -35)

tenant/src/test/java/com/ning/billing/tenant/dao/TestDefaultTenantDao.java 76(+0 -76)

tenant/src/test/java/com/ning/billing/tenant/glue/TestTenantModule.java 37(+0 -37)

tenant/src/test/java/com/ning/billing/tenant/glue/TestTenantModuleNoDB.java 37(+0 -37)

tenant/src/test/java/com/ning/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java 37(+0 -37)

tenant/src/test/java/com/ning/billing/tenant/TenantTestSuiteNoDB.java 34(+0 -34)

tenant/src/test/java/com/ning/billing/tenant/TenantTestSuiteWithEmbeddedDb.java 50(+0 -50)

usage/pom.xml 58(+19 -39)

usage/src/main/java/com/ning/billing/usage/api/user/DefaultRolledUpUsage.java 125(+0 -125)

usage/src/main/java/com/ning/billing/usage/api/user/DefaultUsageUserApi.java 60(+0 -60)

usage/src/main/java/com/ning/billing/usage/dao/DefaultRolledUpUsageDao.java 52(+0 -52)

usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageDao.java 33(+0 -33)

usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageModelDao.java 150(+0 -150)

usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageSqlDao.java 41(+0 -41)

usage/src/main/java/com/ning/billing/usage/glue/UsageModule.java 49(+0 -49)

usage/src/main/resources/com/ning/billing/usage/dao/RolledUpUsageSqlDao.sql.stg 51(+0 -51)

usage/src/main/resources/com/ning/billing/usage/ddl.sql 20(+0 -20)

usage/src/test/java/com/ning/billing/usage/glue/TestUsageModule.java 31(+0 -31)

usage/src/test/java/com/ning/billing/usage/glue/TestUsageModuleNoDB.java 35(+0 -35)

usage/src/test/java/com/ning/billing/usage/glue/TestUsageModuleWithEmbeddedDB.java 35(+0 -35)

usage/src/test/java/com/ning/billing/usage/UsageTestSuiteNoDB.java 44(+0 -44)

usage/src/test/java/com/ning/billing/usage/UsageTestSuiteWithEmbeddedDB.java 44(+0 -44)

util/pom.xml 123(+57 -66)

util/src/main/java/com/ning/billing/util/audit/api/DefaultAuditUserApi.java 99(+0 -99)

util/src/main/java/com/ning/billing/util/audit/dao/AuditDao.java 38(+0 -38)

util/src/main/java/com/ning/billing/util/audit/dao/AuditLogModelDao.java 78(+0 -78)

util/src/main/java/com/ning/billing/util/audit/dao/DefaultAuditDao.java 241(+0 -241)

util/src/main/java/com/ning/billing/util/audit/DefaultAccountAuditLogs.java 175(+0 -175)

util/src/main/java/com/ning/billing/util/audit/DefaultAccountAuditLogsForObjectType.java 151(+0 -151)

util/src/main/java/com/ning/billing/util/audit/DefaultAuditLog.java 124(+0 -124)

util/src/main/java/com/ning/billing/util/bus/DefaultBusService.java 61(+0 -61)

util/src/main/java/com/ning/billing/util/bus/InMemoryBusModule.java 28(+0 -28)

util/src/main/java/com/ning/billing/util/cache/AccountRecordIdCacheLoader.java 61(+0 -61)

util/src/main/java/com/ning/billing/util/cache/AuditLogCacheLoader.java 65(+0 -65)

util/src/main/java/com/ning/billing/util/cache/AuditLogViaHistoryCacheLoader.java 66(+0 -66)

util/src/main/java/com/ning/billing/util/cache/BaseCacheLoader.java 98(+0 -98)

util/src/main/java/com/ning/billing/util/cache/Cachable.java 71(+0 -71)

util/src/main/java/com/ning/billing/util/cache/CachableKey.java 30(+0 -30)

util/src/main/java/com/ning/billing/util/cache/CacheController.java 28(+0 -28)

util/src/main/java/com/ning/billing/util/cache/CacheControllerDispatcher.java 50(+0 -50)

util/src/main/java/com/ning/billing/util/cache/CacheControllerDispatcherProvider.java 72(+0 -72)

util/src/main/java/com/ning/billing/util/cache/CacheLoaderArgument.java 51(+0 -51)

util/src/main/java/com/ning/billing/util/cache/EhCacheBasedCacheController.java 53(+0 -53)

util/src/main/java/com/ning/billing/util/cache/EhCacheCacheManagerProvider.java 77(+0 -77)

util/src/main/java/com/ning/billing/util/cache/ExpirationListenerFactory.java 89(+0 -89)

util/src/main/java/com/ning/billing/util/cache/RecordIdCacheLoader.java 62(+0 -62)

util/src/main/java/com/ning/billing/util/cache/TenantRecordIdCacheLoader.java 61(+0 -61)

util/src/main/java/com/ning/billing/util/callcontext/CallContextFactory.java 37(+0 -37)

util/src/main/java/com/ning/billing/util/callcontext/DefaultCallContextFactory.java 66(+0 -66)

util/src/main/java/com/ning/billing/util/callcontext/InternalCallContextFactory.java 262(+0 -262)

util/src/main/java/com/ning/billing/util/callcontext/InternalTenantContextBinder.java 81(+0 -81)

util/src/main/java/com/ning/billing/util/callcontext/MigrationCallContext.java 43(+0 -43)

util/src/main/java/com/ning/billing/util/config/CacheConfig.java 30(+0 -30)

util/src/main/java/com/ning/billing/util/config/catalog/UriAccessor.java 66(+0 -66)

util/src/main/java/com/ning/billing/util/config/catalog/ValidatingConfig.java 44(+0 -44)

util/src/main/java/com/ning/billing/util/config/catalog/ValidationError.java 61(+0 -61)

util/src/main/java/com/ning/billing/util/config/catalog/ValidationErrors.java 47(+0 -47)

util/src/main/java/com/ning/billing/util/config/catalog/ValidationException.java 40(+0 -40)

util/src/main/java/com/ning/billing/util/config/catalog/XMLLoader.java 114(+0 -114)

util/src/main/java/com/ning/billing/util/config/catalog/XMLSchemaGenerator.java 110(+0 -110)

util/src/main/java/com/ning/billing/util/config/catalog/XMLWriter.java 37(+0 -37)

util/src/main/java/com/ning/billing/util/config/CatalogConfig.java 29(+0 -29)

util/src/main/java/com/ning/billing/util/config/CurrencyConfig.java 29(+0 -29)

util/src/main/java/com/ning/billing/util/config/InvoiceConfig.java 35(+0 -35)

util/src/main/java/com/ning/billing/util/config/KillbillConfig.java 23(+0 -23)

util/src/main/java/com/ning/billing/util/config/OSGIConfig.java 99(+0 -99)

util/src/main/java/com/ning/billing/util/config/PaymentConfig.java 66(+0 -66)

util/src/main/java/com/ning/billing/util/config/RbacConfig.java 30(+0 -30)

util/src/main/java/com/ning/billing/util/config/SecurityConfig.java 84(+0 -84)

util/src/main/java/com/ning/billing/util/config/SubscriptionConfig.java 20(+0 -20)

util/src/main/java/com/ning/billing/util/currency/KillBillMoney.java 36(+0 -36)

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/customfield/dao/CustomFieldDao.java 41(+0 -41)

util/src/main/java/com/ning/billing/util/customfield/dao/CustomFieldModelDao.java 162(+0 -162)

util/src/main/java/com/ning/billing/util/customfield/dao/CustomFieldSqlDao.java 60(+0 -60)

util/src/main/java/com/ning/billing/util/customfield/dao/DefaultCustomFieldDao.java 158(+0 -158)

util/src/main/java/com/ning/billing/util/customfield/ShouldntHappenException.java 24(+0 -24)

util/src/main/java/com/ning/billing/util/customfield/StringCustomField.java 120(+0 -120)

util/src/main/java/com/ning/billing/util/dao/AuditLogModelDaoMapper.java 51(+0 -51)

util/src/main/java/com/ning/billing/util/dao/AuditSqlDao.java 79(+0 -79)

util/src/main/java/com/ning/billing/util/dao/BinderBase.java 33(+0 -33)

util/src/main/java/com/ning/billing/util/dao/DateTimeArgumentFactory.java 68(+0 -68)

util/src/main/java/com/ning/billing/util/dao/DateTimeZoneArgumentFactory.java 66(+0 -66)

util/src/main/java/com/ning/billing/util/dao/DefaultNonEntityDao.java 130(+0 -130)

util/src/main/java/com/ning/billing/util/dao/EntityAudit.java 98(+0 -98)

util/src/main/java/com/ning/billing/util/dao/EntityHistoryBinder.java 77(+0 -77)

util/src/main/java/com/ning/billing/util/dao/EntityHistoryModelDao.java 68(+0 -68)

util/src/main/java/com/ning/billing/util/dao/EnumArgumentFactory.java 60(+0 -60)

util/src/main/java/com/ning/billing/util/dao/HistorySqlDao.java 31(+0 -31)

util/src/main/java/com/ning/billing/util/dao/LocalDateArgumentFactory.java 68(+0 -68)

util/src/main/java/com/ning/billing/util/dao/LowerToCamelBeanMapper.java 173(+0 -173)

util/src/main/java/com/ning/billing/util/dao/LowerToCamelBeanMapperFactory.java 42(+0 -42)

util/src/main/java/com/ning/billing/util/dao/Mapper.java 35(+0 -35)

util/src/main/java/com/ning/billing/util/dao/MapperBase.java 44(+0 -44)

util/src/main/java/com/ning/billing/util/dao/NonEntityDao.java 46(+0 -46)

util/src/main/java/com/ning/billing/util/dao/NonEntitySqlDao.java 79(+0 -79)

util/src/main/java/com/ning/billing/util/dao/RecordIdIdMappings.java 49(+0 -49)

util/src/main/java/com/ning/billing/util/dao/RecordIdIdMappingsMapper.java 41(+0 -41)

util/src/main/java/com/ning/billing/util/dao/TableName.java 97(+0 -97)

util/src/main/java/com/ning/billing/util/dao/UUIDArgumentFactory.java 66(+0 -66)

util/src/main/java/com/ning/billing/util/dao/UuidMapper.java 31(+0 -31)

util/src/main/java/com/ning/billing/util/DefaultAmountFormatter.java 36(+0 -36)

util/src/main/java/com/ning/billing/util/email/DefaultEmailSender.java 98(+0 -98)

util/src/main/java/com/ning/billing/util/email/EmailConfig.java 67(+0 -67)

util/src/main/java/com/ning/billing/util/email/EmailModule.java 41(+0 -41)

util/src/main/java/com/ning/billing/util/email/templates/MustacheTemplateEngine.java 55(+0 -55)

util/src/main/java/com/ning/billing/util/email/templates/TemplateEngine.java 24(+0 -24)

util/src/main/java/com/ning/billing/util/email/templates/TemplateModule.java 28(+0 -28)

util/src/main/java/com/ning/billing/util/entity/dao/Audited.java 47(+0 -47)

util/src/main/java/com/ning/billing/util/entity/dao/DefaultPaginationHelper.java 105(+0 -105)

util/src/main/java/com/ning/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java 71(+0 -71)

util/src/main/java/com/ning/billing/util/entity/dao/EntityDao.java 44(+0 -44)

util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java 172(+0 -172)

util/src/main/java/com/ning/billing/util/entity/dao/EntityModelDao.java 41(+0 -41)

util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDao.java 95(+0 -95)

util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoStringTemplate.java 144(+0 -144)

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/EntitySqlDaoTransactionWrapper.java 31(+0 -31)

util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoWrapperFactory.java 81(+0 -81)

util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoWrapperInvocationHandler.java 431(+0 -431)

util/src/main/java/com/ning/billing/util/entity/DefaultPagination.java 154(+0 -154)

util/src/main/java/com/ning/billing/util/export/api/DefaultExportUserApi.java 54(+0 -54)

util/src/main/java/com/ning/billing/util/export/dao/CSVExportOutputStream.java 138(+0 -138)

util/src/main/java/com/ning/billing/util/export/dao/DatabaseExportDao.java 132(+0 -132)

util/src/main/java/com/ning/billing/util/globallocker/LockerType.java 21(+0 -21)

util/src/main/java/com/ning/billing/util/glue/AuditModule.java 41(+0 -41)

util/src/main/java/com/ning/billing/util/glue/BusModule.java 75(+0 -75)

util/src/main/java/com/ning/billing/util/glue/BusProvider.java 55(+0 -55)

util/src/main/java/com/ning/billing/util/glue/CacheModule.java 49(+0 -49)

util/src/main/java/com/ning/billing/util/glue/CallContextModule.java 31(+0 -31)

util/src/main/java/com/ning/billing/util/glue/ClockModule.java 30(+0 -30)

util/src/main/java/com/ning/billing/util/glue/CustomFieldModule.java 41(+0 -41)

util/src/main/java/com/ning/billing/util/glue/EhCacheManagerProvider.java 51(+0 -51)

util/src/main/java/com/ning/billing/util/glue/ExportModule.java 34(+0 -34)

util/src/main/java/com/ning/billing/util/glue/GlobalLockerModule.java 29(+0 -29)

util/src/main/java/com/ning/billing/util/glue/IniRealmProvider.java 49(+0 -49)

util/src/main/java/com/ning/billing/util/glue/JDBCSessionDaoProvider.java 54(+0 -54)

util/src/main/java/com/ning/billing/util/glue/KillBillShiroAopModule.java 71(+0 -71)

util/src/main/java/com/ning/billing/util/glue/KillBillShiroModule.java 80(+0 -80)

util/src/main/java/com/ning/billing/util/glue/MetricsModule.java 28(+0 -28)

util/src/main/java/com/ning/billing/util/glue/MySqlGlobalLockerProvider.java 39(+0 -39)

util/src/main/java/com/ning/billing/util/glue/NonEntityDaoModule.java 32(+0 -32)

util/src/main/java/com/ning/billing/util/glue/NotificationQueueModule.java 48(+0 -48)

util/src/main/java/com/ning/billing/util/glue/RecordIdModule.java 31(+0 -31)

util/src/main/java/com/ning/billing/util/glue/SecurityModule.java 61(+0 -61)

util/src/main/java/com/ning/billing/util/glue/TagStoreModule.java 51(+0 -51)

util/src/main/java/com/ning/billing/util/Hostname.java 33(+0 -33)

util/src/main/java/com/ning/billing/util/io/IOUtils.java 37(+0 -37)

util/src/main/java/com/ning/billing/util/jackson/ObjectMapper.java 27(+0 -27)

util/src/main/java/com/ning/billing/util/LocaleUtils.java 64(+0 -64)

util/src/main/java/com/ning/billing/util/recordid/DefaultRecordIdApi.java 46(+0 -46)

util/src/main/java/com/ning/billing/util/security/AnnotationHierarchicalResolver.java 130(+0 -130)

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/api/DefaultSecurityApi.java 103(+0 -103)

util/src/main/java/com/ning/billing/util/security/api/DefaultSecurityService.java 52(+0 -52)

util/src/main/java/com/ning/billing/util/security/api/SecurityService.java 22(+0 -22)

util/src/main/java/com/ning/billing/util/security/PermissionAnnotationHandler.java 67(+0 -67)

util/src/main/java/com/ning/billing/util/security/PermissionAnnotationMethodInterceptor.java 28(+0 -28)

util/src/main/java/com/ning/billing/util/security/shiro/dao/JDBCSessionDao.java 87(+0 -87)

util/src/main/java/com/ning/billing/util/security/shiro/dao/JDBCSessionSqlDao.java 46(+0 -46)

util/src/main/java/com/ning/billing/util/security/shiro/dao/SessionModelDao.java 161(+0 -161)

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/svcsapi/bus/BusService.java 24(+0 -24)

util/src/main/java/com/ning/billing/util/tag/api/DefaultTagUserApi.java 199(+0 -199)

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/DefaultUserTagCreationEvent.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/tag/api/user/DefaultUserTagDeletionEvent.java 124(+0 -124)

util/src/main/java/com/ning/billing/util/tag/api/user/TagEventBuilder.java 66(+0 -66)

util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDao.java 233(+0 -233)

util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDefinitionDao.java 272(+0 -272)

util/src/main/java/com/ning/billing/util/tag/dao/TagDao.java 41(+0 -41)

util/src/main/java/com/ning/billing/util/tag/dao/TagDefinitionDao.java 40(+0 -40)

util/src/main/java/com/ning/billing/util/tag/dao/TagDefinitionModelDao.java 138(+0 -138)

util/src/main/java/com/ning/billing/util/tag/dao/TagDefinitionSqlDao.java 54(+0 -54)

util/src/main/java/com/ning/billing/util/tag/dao/TagModelDao.java 154(+0 -154)

util/src/main/java/com/ning/billing/util/tag/dao/TagModelDaoHelper.java 46(+0 -46)

util/src/main/java/com/ning/billing/util/tag/dao/TagSqlDao.java 65(+0 -65)

util/src/main/java/com/ning/billing/util/tag/dao/UUIDCollectionBinder.java 54(+0 -54)

util/src/main/java/com/ning/billing/util/tag/DefaultControlTag.java 77(+0 -77)

util/src/main/java/com/ning/billing/util/tag/DefaultTagDefinition.java 133(+0 -133)

util/src/main/java/com/ning/billing/util/tag/DefaultTagInternalApi.java 88(+0 -88)

util/src/main/java/com/ning/billing/util/tag/DescriptiveTag.java 100(+0 -100)

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/com/ning/billing/util/timezone/DateAndTimeZoneContext.java 80(+0 -80)

util/src/main/java/com/ning/billing/util/userrequest/CompletionUserRequest.java 19(+0 -19)

util/src/main/java/com/ning/billing/util/userrequest/CompletionUserRequestBase.java 167(+0 -167)

util/src/main/java/com/ning/billing/util/userrequest/CompletionUserRequestNotifier.java 28(+0 -28)

util/src/main/java/com/ning/billing/util/userrequest/CompletionUserRequestWaiter.java 51(+0 -51)

util/src/main/java/com/ning/billing/util/validation/dao/DatabaseSchemaDao.java 47(+0 -47)

util/src/main/java/com/ning/billing/util/validation/dao/DatabaseSchemaSqlDao.java 56(+0 -56)

util/src/main/java/com/ning/billing/util/validation/DefaultColumnInfo.java 72(+0 -72)

util/src/main/java/com/ning/billing/util/validation/ValidationConfiguration.java 29(+0 -29)

util/src/main/java/com/ning/billing/util/validation/ValidationManager.java 199(+0 -199)

util/src/main/resources/com/ning/billing/util/customfield/dao/CustomFieldSqlDao.sql.stg 70(+0 -70)

util/src/main/resources/com/ning/billing/util/dao/NonEntitySqlDao.sql.stg 117(+0 -117)

util/src/main/resources/com/ning/billing/util/ddl.sql 240(+0 -240)

util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg 405(+0 -405)

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/tag/dao/TagDefinitionSqlDao.sql.stg 70(+0 -70)

util/src/main/resources/com/ning/billing/util/tag/dao/TagSqlDao.sql.stg 118(+0 -118)

util/src/main/resources/com/ning/billing/util/validation/dao/DatabaseSchemaSqlDao.sql.stg 10(+0 -10)

util/src/test/java/com/ning/billing/api/TestApiListener.java 343(+0 -343)

util/src/test/java/com/ning/billing/api/TestListenerStatus.java 23(+0 -23)

util/src/test/java/com/ning/billing/dao/MockNonEntityDao.java 60(+0 -60)

util/src/test/java/com/ning/billing/dbi/DbiConfig.java 54(+0 -54)

util/src/test/java/com/ning/billing/dbi/DBIProvider.java 65(+0 -65)

util/src/test/java/com/ning/billing/dbi/LoggingOutputStream.java 197(+0 -197)

util/src/test/java/com/ning/billing/DBTestingHelper.java 196(+0 -196)

util/src/test/java/com/ning/billing/GuicyKillbillTestModule.java 54(+0 -54)

util/src/test/java/com/ning/billing/GuicyKillbillTestNoDBModule.java 34(+0 -34)

util/src/test/java/com/ning/billing/GuicyKillbillTestSuite.java 86(+0 -86)

util/src/test/java/com/ning/billing/GuicyKillbillTestSuiteNoDB.java 22(+0 -22)

util/src/test/java/com/ning/billing/GuicyKillbillTestSuiteWithEmbeddedDB.java 72(+0 -72)

util/src/test/java/com/ning/billing/GuicyKillbillTestWithEmbeddedDBModule.java 44(+0 -44)

util/src/test/java/com/ning/billing/KillbillConfigSource.java 63(+0 -63)

util/src/test/java/com/ning/billing/KillbillTestSuite.java 73(+0 -73)

util/src/test/java/com/ning/billing/KillbillTestSuiteWithEmbeddedDB.java 62(+0 -62)

util/src/test/java/com/ning/billing/mock/api/MockAccountUserApi.java 170(+0 -170)

util/src/test/java/com/ning/billing/mock/api/MockExtBusEvent.java 82(+0 -82)

util/src/test/java/com/ning/billing/mock/glue/MockAccountModule.java 46(+0 -46)

util/src/test/java/com/ning/billing/mock/glue/MockClockModule.java 33(+0 -33)

util/src/test/java/com/ning/billing/mock/glue/MockEntitlementModule.java 71(+0 -71)

util/src/test/java/com/ning/billing/mock/glue/MockGlobalLockerModule.java 30(+0 -30)

util/src/test/java/com/ning/billing/mock/glue/MockInvoiceModule.java 58(+0 -58)

util/src/test/java/com/ning/billing/mock/glue/MockJunctionModule.java 36(+0 -36)

util/src/test/java/com/ning/billing/mock/glue/MockNonEntityDaoModule.java 37(+0 -37)

util/src/test/java/com/ning/billing/mock/glue/MockNotificationQueueModule.java 48(+0 -48)

util/src/test/java/com/ning/billing/mock/glue/MockOverdueModule.java 36(+0 -36)

util/src/test/java/com/ning/billing/mock/glue/MockPaymentModule.java 33(+0 -33)

util/src/test/java/com/ning/billing/mock/glue/MockSubscriptionModule.java 67(+0 -67)

util/src/test/java/com/ning/billing/mock/glue/MockTagModule.java 32(+0 -32)

util/src/test/java/com/ning/billing/mock/MockAccountBuilder.java 331(+0 -331)

util/src/test/java/com/ning/billing/mock/MockEffectiveSubscriptionEvent.java 365(+0 -365)

util/src/test/java/com/ning/billing/mock/MockInvoiceFormatterFactory.java 32(+0 -32)

util/src/test/java/com/ning/billing/mock/MockPlan.java 104(+0 -104)

util/src/test/java/com/ning/billing/mock/MockPriceList.java 64(+0 -64)

util/src/test/java/com/ning/billing/mock/MockProduct.java 125(+0 -125)

util/src/test/java/com/ning/billing/mock/MockSubscription.java 233(+0 -233)

util/src/test/java/com/ning/billing/payment/api/TestPaymentMethodPluginBase.java 105(+0 -105)

util/src/test/java/com/ning/billing/payment/plugin/api/PaymentPluginApiWithTestControl.java 26(+0 -26)

util/src/test/java/com/ning/billing/util/audit/api/TestDefaultAuditUserApi.java 72(+0 -72)

util/src/test/java/com/ning/billing/util/audit/AuditLogsTestBase.java 51(+0 -51)

util/src/test/java/com/ning/billing/util/audit/dao/MockAuditDao.java 84(+0 -84)

util/src/test/java/com/ning/billing/util/audit/dao/TestDefaultAuditDao.java 133(+0 -133)

util/src/test/java/com/ning/billing/util/audit/TestDefaultAuditLog.java 87(+0 -87)

util/src/test/java/com/ning/billing/util/cache/TestCache.java 98(+0 -98)

util/src/test/java/com/ning/billing/util/callcontext/TestCallContext.java 89(+0 -89)

util/src/test/java/com/ning/billing/util/callcontext/TestDefaultCallContext.java 69(+0 -69)

util/src/test/java/com/ning/billing/util/callcontext/TestInternalCallContextFactory.java 102(+0 -102)

util/src/test/java/com/ning/billing/util/config/TestXMLLoader.java 55(+0 -55)

util/src/test/java/com/ning/billing/util/config/TestXMLSchemaGenerator.java 38(+0 -38)

util/src/test/java/com/ning/billing/util/config/TestXMLWriter.java 53(+0 -53)

util/src/test/java/com/ning/billing/util/config/XmlTestClass.java 49(+0 -49)

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/customfield/dao/MockCustomFieldDao.java 64(+0 -64)

util/src/test/java/com/ning/billing/util/customfield/MockCustomFieldModuleMemory.java 28(+0 -28)

util/src/test/java/com/ning/billing/util/customfield/TestFieldStore.java 51(+0 -51)

util/src/test/java/com/ning/billing/util/dao/TestNonEntityDao.java 159(+0 -159)

util/src/test/java/com/ning/billing/util/dao/TestPagination.java 77(+0 -77)

util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritance.java 192(+0 -192)

util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritanceWithJdbi.java 52(+0 -52)

util/src/test/java/com/ning/billing/util/email/DefaultCatalogTranslationTest.java 102(+0 -102)

util/src/test/java/com/ning/billing/util/email/EmailSenderTest.java 46(+0 -46)

util/src/test/java/com/ning/billing/util/entity/dao/MockEntityDaoBase.java 98(+0 -98)

util/src/test/java/com/ning/billing/util/entity/TestDefaultPagination.java 53(+0 -53)

util/src/test/java/com/ning/billing/util/export/dao/TestCSVExportOutputStream.java 69(+0 -69)

util/src/test/java/com/ning/billing/util/export/dao/TestDatabaseExportDao.java 98(+0 -98)

util/src/test/java/com/ning/billing/util/globallocker/TestGlobalLockerModule.java 41(+0 -41)

util/src/test/java/com/ning/billing/util/globallocker/TestMysqlGlobalLocker.java 66(+0 -66)

util/src/test/java/com/ning/billing/util/glue/TestUtilModule.java 46(+0 -46)

util/src/test/java/com/ning/billing/util/glue/TestUtilModuleNoDB.java 58(+0 -58)

util/src/test/java/com/ning/billing/util/glue/TestUtilModuleWithEmbeddedDB.java 43(+0 -43)

util/src/test/java/com/ning/billing/util/security/api/TestDefaultSecurityApi.java 52(+0 -52)

util/src/test/java/com/ning/billing/util/security/shiro/dao/TestJDBCSessionDao.java 74(+0 -74)

util/src/test/java/com/ning/billing/util/security/shiro/dao/TestSessionModelDao.java 54(+0 -54)

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/tag/api/user/TestTagEventBuilder.java 230(+0 -230)

util/src/test/java/com/ning/billing/util/tag/dao/MockTagDao.java 102(+0 -102)

util/src/test/java/com/ning/billing/util/tag/dao/MockTagDefinitionDao.java 64(+0 -64)

util/src/test/java/com/ning/billing/util/tag/dao/TestDefaultTagDao.java 179(+0 -179)

util/src/test/java/com/ning/billing/util/tag/dao/TestDefaultTagDefinitionDao.java 106(+0 -106)

util/src/test/java/com/ning/billing/util/tag/TestTagStore.java 152(+0 -152)

util/src/test/java/com/ning/billing/util/template/translation/TestDefaultTranslatorBase.java 53(+0 -53)

util/src/test/java/com/ning/billing/util/timezone/TestDateAndTimeZoneContext.java 213(+0 -213)

util/src/test/java/com/ning/billing/util/UtilTestSuiteNoDB.java 120(+0 -120)

util/src/test/java/com/ning/billing/util/UtilTestSuiteWithEmbeddedDB.java 140(+0 -140)

util/src/test/java/com/ning/billing/util/validation/TestValidationManager.java 178(+0 -178)

util/src/test/resources/com/ning/billing/util/dao/Kombucha.sql.stg 21(+0 -21)

util/src/test/resources/com/ning/billing/util/ddl_test.sql 35(+0 -35)

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)

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
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>
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();
+    }
+}
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
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 
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);
+    }
+}
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;
+            }
+        }
+    }
+}
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();
+    }
+}
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();
+    }
+}
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>
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>
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);
+    }
+}
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>
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 {
 
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>
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 {
+    }
+}
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;
+    }
+}
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>
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
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)
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();
+            }
+        }
+    }
+}
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
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()));
+    }
+}
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
+;
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