diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dbb6264..0237023 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,6 @@ jobs:
with:
plugin_name: 'avatax'
integration_tests_goal: 'test:plugins:avatax'
- ddl_file: 'src/main/resources/ddl.sql'
integration_tests_ref: 'refs/heads/master'
secrets:
extra-env: '{ "AVATAX_URL": "${{ secrets.AVATAX_URL }}", "AVATAX_ACCOUNT_ID": "${{ secrets.AVATAX_ACCOUNT_ID }}", "AVATAX_LICENSE_KEY": "${{ secrets.AVATAX_LICENSE_KEY }}", "AVATAX_COMPANY_CODE": "${{ secrets.AVATAX_COMPANY_CODE }}", "AVATAX_COMMIT_DOCUMENTS": false, "AVATAX_TAXRATESAPI_URL": "${{ secrets.AVATAX_TAXRATESAPI_URL }}", "AVATAX_TAXRATESAPI_ACCOUNT_ID": "${{ secrets.AVATAX_TAXRATESAPI_ACCOUNT_ID }}", "AVATAX_TAXRATESAPI_LICENSE_KEY": "${{ secrets.AVATAX_TAXRATESAPI_LICENSE_KEY }}" }'
diff --git a/README.md b/README.md
index 5fb6985..02d0cbc 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,8 @@ We've upgraded numerous dependencies in 0.8.x (required for Java 11 support).
## Requirements
-The plugin needs a database. The latest version of the schema can be found [here](https://github.com/killbill/killbill-avatax-plugin/blob/master/src/main/resources/ddl.sql).
+* An active Avatax account is required to use the plugin.
+* 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 [src/main/resources/migration](src/main/resources/migration) directory to create or update the database tables as needed. See the [Database Setup](#database-setup) section below for details about disabling automatic migrations.
## Development
@@ -81,6 +82,19 @@ org.killbill.billing.plugin.avatax.licenseKey=ZZZ' \
Refer to the [Avatax Plugin Manual](https://docs.killbill.io/latest/avatax-plugin#plugin_configuration) for further details.
+## Database Setup
+
+The Avatax plugin requires a database. By default, schema migrations run automatically at plugin startup.
+
+To skip automatic migrations (for example, if you prefer to manage the database schema manually), ensure that the following property is set before starting the plugin the first time:
+
+```properties
+org.killbill.billing.plugin.avatax.runMigrations=false
+```
+
+
+When automatic migrations are disabled, ensure that the required database tables are created manually using the SQL scripts provided in the [src/main/resources/migration](src/main/resources/migration) directory.
+
## AvaTax tax calculation details
Taxes are calculated by default using the address specified on the Kill Bill account (set as the `shipTo` element). In case your current e-commerce application doesn't validate addresses, you can use [Avalara's Address Validation service](https://developer.avalara.com/avatax/address-validation/) to do it (Avalara will implicitly validate addresses during the tax calculation and fail the invoice creation if the address is invalid).
diff --git a/pom.xml b/pom.xml
index 7bfbb58..ecd0a99 100644
--- a/pom.xml
+++ b/pom.xml
@@ -109,6 +109,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/avatax/core/AvaTaxActivator.java b/src/main/java/org/killbill/billing/plugin/avatax/core/AvaTaxActivator.java
index 624175a..833b02e 100644
--- a/src/main/java/org/killbill/billing/plugin/avatax/core/AvaTaxActivator.java
+++ b/src/main/java/org/killbill/billing/plugin/avatax/core/AvaTaxActivator.java
@@ -18,11 +18,13 @@
package org.killbill.billing.plugin.avatax.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.invoice.plugin.api.InvoicePluginApi;
import org.killbill.billing.osgi.api.Healthcheck;
import org.killbill.billing.osgi.api.OSGIPluginProperties;
@@ -34,17 +36,25 @@
import org.killbill.billing.plugin.avatax.dao.AvaTaxDao;
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.killbill.clock.Clock;
import org.killbill.clock.DefaultClock;
import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class AvaTaxActivator extends KillbillActivatorBase {
+ private static final Logger logger = LoggerFactory.getLogger(AvaTaxActivator.class);
+
public static final String PLUGIN_NAME = "killbill-avatax";
public static final String PROPERTY_PREFIX = "org.killbill.billing.plugin.avatax.";
public static final String TAX_RATES_API_PROPERTY_PREFIX = "org.killbill.billing.plugin.avatax.taxratesapi.";
+ private static final String SHOULD_RUN_MIGRATIONS_PROPERTY = PROPERTY_PREFIX + "runMigrations";
+
private AvaTaxConfigurationHandler avaTaxConfigurationHandler;
private TaxRatesConfigurationHandler taxRatesConfigurationHandler;
@@ -52,6 +62,8 @@ public class AvaTaxActivator extends KillbillActivatorBase {
public void start(final BundleContext context) throws Exception {
super.start(context);
+ runMigrationsIfEnabled();
+
final AvaTaxDao dao = new AvaTaxDao(dataSource.getDataSource());
final Clock clock = new DefaultClock();
@@ -119,4 +131,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() {
+ if (Boolean.parseBoolean(configProperties.getProperties().getProperty(SHOULD_RUN_MIGRATIONS_PROPERTY, "true"))) {
+ 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("avatax_schema_history")
+ .baselineOnMigrate(true)
+ .baselineVersion("0")
+ .load();
+ flyway.migrate();
+ } else {
+ logger.info("Skipping Flyway migrations as '{}' is set to false", SHOULD_RUN_MIGRATIONS_PROPERTY);
+ }
+ }
}
diff --git a/src/main/resources/migration/mysql/V1__Create_avatax_tables.sql b/src/main/resources/migration/mysql/V1__Create_avatax_tables.sql
new file mode 100644
index 0000000..e5574b5
--- /dev/null
+++ b/src/main/resources/migration/mysql/V1__Create_avatax_tables.sql
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020-2026 Equinix, Inc
+ * Copyright 2014-2026 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.
+ */
+
+create table avatax_responses (
+ record_id serial unique
+, kb_account_id char(36) not null
+, kb_invoice_id char(36) not null
+, kb_invoice_item_ids longtext default null
+, doc_code varchar(255) default null
+, doc_date datetime default null
+, timestamp datetime default null
+, total_amount numeric(15,9) default null
+, total_discount numeric(15,9) default null
+, total_exemption numeric(15,9) default null
+, total_taxable numeric(15,9) default null
+, total_tax numeric(15,9) default null
+, total_tax_calculated numeric(15,9) default null
+, tax_date datetime default null
+, tax_lines longtext default null
+, tax_summary longtext default null
+, tax_addresses longtext default null
+, result_code varchar(255) default null
+, messages longtext default 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 avatax_responses_kb_account_id on avatax_responses(kb_account_id);
+create index avatax_responses_kb_invoice_id on avatax_responses(kb_invoice_id);
+
+create table avatax_tax_codes (
+ record_id serial unique
+, product_name varchar(255) not null
+, tax_code varchar(255) not 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 avatax_tax_codes_product_name on avatax_tax_codes(product_name);
+create unique index avatax_tax_codes_product_name_tax_code_kb_tenant_id on avatax_tax_codes(product_name, tax_code, kb_tenant_id);
+
diff --git a/src/main/resources/migration/postgresql/V1__Create_avatax_tables.sql b/src/main/resources/migration/postgresql/V1__Create_avatax_tables.sql
new file mode 100644
index 0000000..8b78cf3
--- /dev/null
+++ b/src/main/resources/migration/postgresql/V1__Create_avatax_tables.sql
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2020-2026 Equinix, Inc
+ * Copyright 2014-2026 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.
+ */
+
+/*
+ * Copyright 2014-2020 Groupon, Inc
+ * 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.
+ */
+
+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 avatax_responses (
+ record_id bigserial unique
+, kb_account_id char(36) not null
+, kb_invoice_id char(36) not null
+, kb_invoice_item_ids longtext default null
+, doc_code varchar(255) default null
+, doc_date datetime default null
+, timestamp datetime default null
+, total_amount numeric(15,9) default null
+, total_discount numeric(15,9) default null
+, total_exemption numeric(15,9) default null
+, total_taxable numeric(15,9) default null
+, total_tax numeric(15,9) default null
+, total_tax_calculated numeric(15,9) default null
+, tax_date datetime default null
+, tax_lines longtext default null
+, tax_summary longtext default null
+, tax_addresses longtext default null
+, result_code varchar(255) default null
+, messages longtext default null
+, additional_data longtext default null
+, created_date datetime not null
+, kb_tenant_id char(36) not null
+, primary key(record_id)
+);
+create index avatax_responses_kb_account_id on avatax_responses(kb_account_id);
+create index avatax_responses_kb_invoice_id on avatax_responses(kb_invoice_id);
+
+create table avatax_tax_codes (
+ record_id bigserial unique
+, product_name varchar(255) not null
+, tax_code varchar(255) not null
+, created_date datetime not null
+, kb_tenant_id char(36) not null
+, primary key(record_id)
+);
+create index avatax_tax_codes_product_name on avatax_tax_codes(product_name);
+create unique index avatax_tax_codes_product_name_tax_code_kb_tenant_id on avatax_tax_codes(product_name, tax_code, kb_tenant_id);
+
diff --git a/src/test/java/org/killbill/billing/plugin/avatax/client/TestTaxRatesClient.java b/src/test/java/org/killbill/billing/plugin/avatax/client/TestTaxRatesClient.java
index 08e6126..4cf2f20 100644
--- a/src/test/java/org/killbill/billing/plugin/avatax/client/TestTaxRatesClient.java
+++ b/src/test/java/org/killbill/billing/plugin/avatax/client/TestTaxRatesClient.java
@@ -18,6 +18,9 @@
package org.killbill.billing.plugin.avatax.client;
+import java.util.Map;
+import java.util.stream.Collectors;
+
import org.killbill.billing.plugin.avatax.AvaTaxRemoteTestBase;
import org.killbill.billing.plugin.avatax.client.model.TaxRateResult;
import org.testng.Assert;
@@ -41,20 +44,11 @@ private void checkSFRates(final TaxRateResult result) {
Assert.assertEquals(result.totalRate, 0.08625);
Assert.assertEquals(result.rates.size(), 4);
- Assert.assertEquals(result.rates.get(0).rate, 0.0025);
- Assert.assertEquals(result.rates.get(0).name, "CA COUNTY TAX");
- Assert.assertEquals(result.rates.get(0).type, "County");
-
- Assert.assertEquals(result.rates.get(1).rate, 0.06);
- Assert.assertEquals(result.rates.get(1).name, "CA STATE TAX");
- Assert.assertEquals(result.rates.get(1).type, "State");
-
- Assert.assertEquals(result.rates.get(2).rate, 0.01375);
- Assert.assertEquals(result.rates.get(2).name, "CA SPECIAL TAX");
- Assert.assertEquals(result.rates.get(2).type, "Special");
+ final Map rateByName = result.rates.stream()
+ .collect(Collectors.toMap(r -> r.name, r -> r.rate, (a, b) -> a));
- Assert.assertEquals(result.rates.get(3).rate, 0.01);
- Assert.assertEquals(result.rates.get(3).name, "CA SPECIAL TAX");
- Assert.assertEquals(result.rates.get(3).type, "Special");
+ Assert.assertEquals(rateByName.get("CA STATE TAX"), 0.06);
+ Assert.assertEquals(rateByName.get("CA COUNTY TAX"), 0.0025);
+ Assert.assertEquals(rateByName.get("CA SPECIAL TAX"), 0.01375);
}
}