diff --git a/README.md b/README.md index d6e3c80..5b3a44e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ A full end-to-end integration demo is available [here](https://github.com/killbi ## Requirements * An active Braintree account is required for using the plugin. A Braintree sandbox account may be used for testing purposes. -* The plugin needs a database. The latest version of the schema can be found [here](https://github.com/killbill/killbill-braintree/tree/master/src/main/resources). +* The plugin needs a database. The database tables are automatically created and updated at plugin startup by default. Alternatively, if you would like to manage the database schema manually, you can disable automatic migrations and use the SQL scripts provided in the (https://github.com/killbill/killbill-braintree/tree/master/src/main/resources/migration)[src/main/resources/migration] directory to create or update the database tables as needed. See the [Configuration](#configuration) section below for details about disabling automatic migrations. + ## Build @@ -73,6 +74,23 @@ Some important notes: * The plugin attempts to load the credentials either from the per-tenant configuration or the Kill Bill properties file while the unit tests require the properties to be set as environment variables. * In order to facilitate automated testing, you should disable all fraud detection within your sandbox account. These can generate gateway rejection errors when processing multiple test transactions. In particular make sure to disable [Duplicate Transaction Checking](https://articles.braintreepayments.com/control-panel/transactions/duplicate-checking#configuring-duplicate-transaction-checking). +In addition, the `org.killbill.billing.plugin.braintree.runMigrations` property can be used to control Flyway DB migrations at plugin startup. This property eliminates the need to manually install or update the database tables, as the plugin will handle schema setup automatically. + +Default value: + +```properties +org.killbill.billing.plugin.braintree.runMigrations=true +``` + +To skip automatic migrations (for example, if you prefer to manage the database schema manually), set: + +```properties +org.killbill.billing.plugin.braintree.runMigrations=false +``` + + +When `org.killbill.billing.plugin.braintree.runMigrations` is disabled, ensure that the required database tables are created manually using the SQL scripts provided in the (https://github.com/killbill/killbill-braintree/tree/master/src/main/resources/migration)[src/main/resources/migration] directory. + ## Testing 1. Ensure that the plugin is installed and configured as explained above. @@ -223,6 +241,3 @@ curl -v \ -H "X-Killbill-Comment: demo" \ "http://127.0.0.1:8080/1.0/kb/accounts//paymentMethods/refresh" ``` - - - diff --git a/pom.xml b/pom.xml index f71f808..6575bf8 100644 --- a/pom.xml +++ b/pom.xml @@ -112,6 +112,11 @@ org.apache.felix.framework provided + + org.flywaydb + flyway-core + 7.7.3 + org.jooby jooby diff --git a/src/main/java/org/killbill/billing/plugin/braintree/core/BraintreeActivator.java b/src/main/java/org/killbill/billing/plugin/braintree/core/BraintreeActivator.java index 3e04133..2e8833f 100644 --- a/src/main/java/org/killbill/billing/plugin/braintree/core/BraintreeActivator.java +++ b/src/main/java/org/killbill/billing/plugin/braintree/core/BraintreeActivator.java @@ -16,11 +16,13 @@ package org.killbill.billing.plugin.braintree.core; +import java.sql.SQLException; import java.util.Hashtable; import javax.servlet.Servlet; import javax.servlet.http.HttpServlet; +import org.flywaydb.core.Flyway; import org.killbill.billing.osgi.api.Healthcheck; import org.killbill.billing.osgi.api.OSGIPluginProperties; import org.killbill.billing.osgi.libs.killbill.KillbillActivatorBase; @@ -33,6 +35,8 @@ import org.killbill.billing.plugin.core.config.PluginEnvironmentConfig; import org.killbill.billing.plugin.core.resources.jooby.PluginApp; import org.killbill.billing.plugin.core.resources.jooby.PluginAppBuilder; +import org.killbill.billing.plugin.dao.PluginDao; +import org.killbill.billing.plugin.dao.PluginDao.DBEngine; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +53,7 @@ public void start(final BundleContext context) throws Exception { super.start(context); final String region = PluginEnvironmentConfig.getRegion(configProperties.getProperties()); + runMigrationsIfEnabled(); // Register an event listener for plugin configuration braintreeConfigurationHandler = new BraintreeConfigPropertiesConfigurationHandler(region, PLUGIN_NAME, killbillAPI); @@ -108,4 +113,41 @@ private void registerHealthcheck(final BundleContext context, final Healthcheck props.put(OSGIPluginProperties.PLUGIN_NAME_PROP, PLUGIN_NAME); registrar.registerService(context, Healthcheck.class, healthcheck, props); } + + private void runMigrationsIfEnabled() { + // Run Flyway migrations to create/update database tables + if (BraintreeConfigProperties.shouldRunMigrations(configProperties.getProperties())) { + DBEngine dbEngine; + try { + dbEngine = PluginDao.getDBEngine(dataSource.getDataSource()); + } catch (final SQLException e) { + logger.warn("Unable to determine database engine, defaulting to MySQL migrations", e); + dbEngine = DBEngine.MYSQL; + } + + final String locations; + switch (dbEngine) { + case POSTGRESQL: + locations = "classpath:migration/postgresql"; + break; + case GENERIC: + case H2: + case MYSQL: + default: + // H2 and GENERIC use MySQL-compatible migration scripts + locations = "classpath:migration/mysql"; + break; + } + + final Flyway flyway = Flyway.configure(getClass().getClassLoader()) + .dataSource(dataSource.getDataSource()) + .locations(locations) + .table("braintree_schema_history") + .baselineOnMigrate(true) + .load(); + flyway.migrate(); + } else { + logger.info("Skipping Flyway migrations as 'runMigrations' is set to false"); + } + } } diff --git a/src/main/java/org/killbill/billing/plugin/braintree/core/BraintreeConfigProperties.java b/src/main/java/org/killbill/billing/plugin/braintree/core/BraintreeConfigProperties.java index 983142b..e56aea1 100644 --- a/src/main/java/org/killbill/billing/plugin/braintree/core/BraintreeConfigProperties.java +++ b/src/main/java/org/killbill/billing/plugin/braintree/core/BraintreeConfigProperties.java @@ -42,7 +42,8 @@ public class BraintreeConfigProperties { private static final String KEY_VALUE_DELIMITER = "#"; private static final String DEFAULT_CONNECTION_TIMEOUT = "30000"; private static final String DEFAULT_READ_TIMEOUT = "60000"; - + private static final String DEFAULT_RUN_MIGRATIONS = "true"; + private final String region; private final String btEnvironment; private final String btMerchantId; @@ -54,7 +55,8 @@ public class BraintreeConfigProperties { private final Map paymentMethodToExpirationPeriod = new LinkedHashMap(); private final String chargeDescription; private final String chargeStatementDescriptor; - + private final boolean runMigrations; + public BraintreeConfigProperties(final Properties properties, final String region) { this.region = region; this.btEnvironment = properties.getProperty(PROPERTY_PREFIX + "btEnvironment", "sandbox"); @@ -66,6 +68,7 @@ public BraintreeConfigProperties(final Properties properties, final String regio this.pendingPaymentExpirationPeriod = readPendingExpirationProperty(properties); this.chargeDescription = Ascii.truncate(MoreObjects.firstNonNull(properties.getProperty(PROPERTY_PREFIX + "chargeDescription"), "Kill Bill charge"), 22, "..."); this.chargeStatementDescriptor = Ascii.truncate(MoreObjects.firstNonNull(properties.getProperty(PROPERTY_PREFIX + "chargeStatementDescriptor"), "Kill Bill charge"), 22, "..."); + this.runMigrations = Boolean.parseBoolean(properties.getProperty(PROPERTY_PREFIX + "runMigrations", DEFAULT_RUN_MIGRATIONS)); } public String getRegion() { @@ -116,6 +119,14 @@ public String getChargeStatementDescriptor() { return chargeStatementDescriptor; } + public boolean isRunMigrations() { + return runMigrations; + } + + public static boolean shouldRunMigrations(final Properties properties) { + return Boolean.parseBoolean(properties.getProperty(PROPERTY_PREFIX + "runMigrations", DEFAULT_RUN_MIGRATIONS)); + } + public Period getPendingPaymentExpirationPeriod(@Nullable final String paymentMethod) { if (paymentMethod != null && paymentMethodToExpirationPeriod.get(paymentMethod.toLowerCase()) != null) { return paymentMethodToExpirationPeriod.get(paymentMethod.toLowerCase()); diff --git a/src/main/resources/ddl-postgresql.sql b/src/main/resources/ddl-postgresql.sql index 897ce5d..a9da5c2 100644 --- a/src/main/resources/ddl-postgresql.sql +++ b/src/main/resources/ddl-postgresql.sql @@ -16,6 +16,12 @@ */ /* We cannot use timestamp in MySQL because of the implicit TimeZone conversions it does behind the scenes */ -CREATE DOMAIN datetime AS timestamp without time zone; +DO $$ BEGIN + CREATE DOMAIN datetime AS timestamp without time zone; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; -CREATE DOMAIN longtext AS text; +DO $$ BEGIN + CREATE DOMAIN longtext AS text; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; diff --git a/src/main/resources/migration/mysql/V20200101000000__create_tables.sql b/src/main/resources/migration/mysql/V20200101000000__create_tables.sql new file mode 100644 index 0000000..8f6e8fc --- /dev/null +++ b/src/main/resources/migration/mysql/V20200101000000__create_tables.sql @@ -0,0 +1,48 @@ +/* + * Copyright 2021 Wovenware, Inc + * + * Wovenware licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +create table braintree_responses ( + record_id serial +, kb_account_id char(36) not null +, kb_payment_id char(36) not null +, kb_payment_transaction_id char(36) not null +, transaction_type varchar(32) not null +, amount numeric(15,9) +, currency char(3) +, braintree_id varchar(255) not null +, additional_data longtext default null +, created_date datetime not null +, kb_tenant_id char(36) not null +, primary key(record_id) +) /*! CHARACTER SET utf8 COLLATE utf8_bin */; +create index braintree_responses_kb_payment_id on braintree_responses(kb_payment_id); +create index braintree_responses_kb_payment_transaction_id on braintree_responses(kb_payment_transaction_id); +create index braintree_responses_braintree_id on braintree_responses(braintree_id); + +create table braintree_payment_methods ( + record_id serial +, kb_account_id char(36) not null +, kb_payment_method_id char(36) not null +, braintree_id varchar(255) not null +, is_deleted smallint not null default 0 +, additional_data longtext default null +, created_date datetime not null +, updated_date datetime not null +, kb_tenant_id char(36) not null +, primary key(record_id) +) /*! CHARACTER SET utf8 COLLATE utf8_bin */; +create unique index braintree_payment_methods_kb_payment_id on braintree_payment_methods(kb_payment_method_id); +create index braintree_payment_methods_braintree_id on braintree_payment_methods(braintree_id); diff --git a/src/main/resources/migration/V20200127071214__add_is_default_to_braintree_payment_methods.sql b/src/main/resources/migration/mysql/V20200127071214__add_is_default_to_braintree_payment_methods.sql similarity index 100% rename from src/main/resources/migration/V20200127071214__add_is_default_to_braintree_payment_methods.sql rename to src/main/resources/migration/mysql/V20200127071214__add_is_default_to_braintree_payment_methods.sql diff --git a/src/main/resources/migration/postgresql/V20200101000000__create_tables.sql b/src/main/resources/migration/postgresql/V20200101000000__create_tables.sql new file mode 100644 index 0000000..2037644 --- /dev/null +++ b/src/main/resources/migration/postgresql/V20200101000000__create_tables.sql @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2020 Equinix, Inc + * Copyright 2014-2020 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* We cannot use timestamp in MySQL because of the implicit TimeZone conversions it does behind the scenes */ +DO $$ BEGIN + CREATE DOMAIN datetime AS timestamp without time zone; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + CREATE DOMAIN longtext AS text; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +create table braintree_responses ( + record_id serial + , kb_account_id char(36) not null + , kb_payment_id char(36) not null + , kb_payment_transaction_id char(36) not null + , transaction_type varchar(32) not null + , amount numeric(15,9) + , currency char(3) + , braintree_id varchar(255) not null + , additional_data longtext default null + , created_date datetime not null + , kb_tenant_id char(36) not null + , primary key(record_id) +) /*! CHARACTER SET utf8 COLLATE utf8_bin */; +create index braintree_responses_kb_payment_id on braintree_responses(kb_payment_id); +create index braintree_responses_kb_payment_transaction_id on braintree_responses(kb_payment_transaction_id); +create index braintree_responses_braintree_id on braintree_responses(braintree_id); + +create table braintree_payment_methods ( + record_id serial + , kb_account_id char(36) not null + , kb_payment_method_id char(36) not null + , braintree_id varchar(255) not null + , is_deleted smallint not null default 0 + , additional_data longtext default null + , created_date datetime not null + , updated_date datetime not null + , kb_tenant_id char(36) not null + , primary key(record_id) +) /*! CHARACTER SET utf8 COLLATE utf8_bin */; +create unique index braintree_payment_methods_kb_payment_id on braintree_payment_methods(kb_payment_method_id); +create index braintree_payment_methods_braintree_id on braintree_payment_methods(braintree_id); + diff --git a/src/main/resources/migration/postgresql/V20200127071214__add_is_default_to_braintree_payment_methods.sql b/src/main/resources/migration/postgresql/V20200127071214__add_is_default_to_braintree_payment_methods.sql new file mode 100644 index 0000000..f806c7e --- /dev/null +++ b/src/main/resources/migration/postgresql/V20200127071214__add_is_default_to_braintree_payment_methods.sql @@ -0,0 +1,18 @@ +/* + * Copyright 2020-2020 Equinix, Inc + * Copyright 2014-2020 The Billing Project, LLC + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +ALTER TABLE braintree_payment_methods ADD COLUMN is_default SMALLINT NOT NULL DEFAULT 0; \ No newline at end of file