diff --git a/CHANGELOG.md b/CHANGELOG.md index e8f15f7..416ca29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## 2.1.4 +### Added +- Allow for live and sandbox credentials to be set as ENV variables with autosuggest fields in the plugin settings to keep sensitive info out of Project Config. [#11](https://github.com/surprisehighway/craft-avatax/issues/11) [30](https://github.com/surprisehighway/craft-avatax/issues/30) +- Update config override example to default to multi-environment settings with ENV examples. +- For new installs the address validation setting will be disabled by default. + +### Fixed +- Better error handling. No longer throw exceptions on the front end but log more complete error responses. [#29](https://github.com/surprisehighway/craft-avatax/issues/29) [#31](https://github.com/surprisehighway/craft-avatax/issues/31) + ## 2.1.3 ### Fixed - Fixed an issue where updating the shipping state did not refresh the cached order. [#27](https://github.com/surprisehighway/craft-avatax/issues/27) diff --git a/README.md b/README.md index 354034b..eb705a6 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ Setup and configuration is detailed below, but here's a quick overview of what y 4. Click the *Test Connection* button to verify your connection. 5. Click the *Save* button to save your settings. +> Hint: it is recommended to use [Environment Variables](https://craftcms.com/docs/3.x/config/#environmental-configuration) for the following settings: Account ID, License Key, Company Code, Sandbox Account ID, Sandbox License Key, Sandbox Company Code to prevent sensitive info being saved in Project Config or the database. See [Config Overrides](#config-overrides) below for more examples. + ![Account Settings](resources/img/plugin-settings.png) ## Configuring AvaTax Ship From Origin @@ -160,6 +162,78 @@ You can use Craft's [plugin config file](https://docs.craftcms.com/v3/extend/plu 1. Copy `config.php` from the `avataxtax` directory to your craft/config folder and rename it to `avatax.php` 2. Update values in `avatax.php` and save. +It is recommended to use ENV variables to following account settings: + +.ENV file: +``` +AVATAX_ACCOUNT_ID=123456 +AVATAX_LICENSE_KEY=987654321 +AVATAX_COMPANY_CODE=MYCOMPANY +AVATAX_SANDBOX_ACCOUNT_ID=123456 +AVATAX_SANDBOX_LICENSE_KEY=987654321 +AVATAX_SANDBOX_COMPANY_CODE=MYCOMPANY + +``` + +config/avatax.php: +``` + [ + // The address you will be posting from. + 'shipFromName' => 'John Doe', + 'shipFromStreet1' => '201 E Randolph St', + 'shipFromStreet2' => '', + 'shipFromStreet3' => '', + 'shipFromCity' => 'Chicago', + 'shipFromState' => 'IL', + 'shipFromZipCode' => '60601', + 'shipFromCountry' => 'US', + + // The default Avalara Tax Code to use for Products. + 'defaultTaxCode' => 'P0000000', + + // The default Avalara Tax Code to use for Shipping. + 'defaultShippingCode' => 'FR', + + // The default Avalara Tax Code to use for Discounts. + 'defaultDiscountCode' => 'OD010000', + + // Production account information from ENV. + 'accountId' => '$AVATAX_ACCOUNT_ID', + 'licenseKey' => '$AVATAX_LICENSE_KEY', + 'companyCode' => '$AVATAX_COMPANY_CODE', + + // Sandbox account information from ENV. + 'sandboxAccountId' => '$AVATAX_SANDBOX_ACCOUNT_ID', + 'sandboxLicenseKey' => '$AVATAX_SANDBOX_LICENSE_KEY', + 'sandboxCompanyCode' => '$AVATAX_SANDBOX_COMPANY_CODE', + + // Environment - 'production' or 'sandbox'. + 'environment' => 'sandbox', + + // AvaTax options - true or false + 'enableTaxCalculation' => true, + 'enableCommitting' => true, + 'enableAddressValidation' => false, + 'enablePartialRefunds' => true, + + // Enable debugging - true or false + 'debug' => true, + ], + + 'production' => [ + // Environment - 'production' or 'sandbox'. + 'environment' => 'production', + + // Enable debugging - true or false + 'debug' => false, + ], + +]; +``` + ## Ajax Examples There is a JSON controller endpoint you can use for AJAX lookups/validation on the front-end. Currently the only endpoints are for address validation and [CertCapture](https://certcapture6xrest.docs.apiary.io) customer lookup by customer number. diff --git a/composer.json b/composer.json index 2adae7b..9692a41 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "surprisehighway/craft-avatax", "description": "Calculate and add sales tax to an order's base tax using Avalara's Avatax service.", "type": "craft-plugin", - "version": "2.1.3", + "version": "2.1.4", "keywords": [ "craft", "cms", diff --git a/resources/img/plugin-settings.png b/resources/img/plugin-settings.png index e3d171b..3683ed6 100644 Binary files a/resources/img/plugin-settings.png and b/resources/img/plugin-settings.png differ diff --git a/src/config.php b/src/config.php index 36c6209..2b5d18a 100644 --- a/src/config.php +++ b/src/config.php @@ -23,45 +23,55 @@ */ return [ - // The address you will be posting from. - 'shipFromName' => 'John Doe', - 'shipFromStreet1' => '201 E Randolph St', - 'shipFromStreet2' => '', - 'shipFromStreet3' => '', - 'shipFromCity' => 'Chicago', - 'shipFromState' => 'IL', - 'shipFromZipCode' => '60601', - 'shipFromCountry' => 'US', + '*' => [ + // The address you will be posting from. + 'shipFromName' => 'John Doe', + 'shipFromStreet1' => '201 E Randolph St', + 'shipFromStreet2' => '', + 'shipFromStreet3' => '', + 'shipFromCity' => 'Chicago', + 'shipFromState' => 'IL', + 'shipFromZipCode' => '60601', + 'shipFromCountry' => 'US', - // The default Avalara Tax Code to use for Products. - 'defaultTaxCode' => 'P0000000', + // The default Avalara Tax Code to use for Products. + 'defaultTaxCode' => 'P0000000', - // The default Avalara Tax Code to use for Shipping. - 'defaultShippingCode' => 'FR', + // The default Avalara Tax Code to use for Shipping. + 'defaultShippingCode' => 'FR', - // The default Avalara Tax Code to use for Discounts. - 'defaultDiscountCode' => 'OD010000', + // The default Avalara Tax Code to use for Discounts. + 'defaultDiscountCode' => 'OD010000', - // Production account information. - 'accountId' => '', - 'licenseKey' => '', - 'companyCode' => '', + // Production account information from ENV. + 'accountId' => '$AVATAX_ACCOUNT_ID', + 'licenseKey' => '$AVATAX_LICENSE_KEY', + 'companyCode' => '$AVATAX_COMPANY_CODE', - // Sandbox account information. - 'sandboxAccountId' => '', - 'sandboxLicenseKey' => '', - 'sandboxCompanyCode' => '', + // Sandbox account information from ENV. + 'sandboxAccountId' => '$AVATAX_SANDBOX_ACCOUNT_ID', + 'sandboxLicenseKey' => '$AVATAX_SANDBOX_LICENSE_KEY', + 'sandboxCompanyCode' => '$AVATAX_SANDBOX_COMPANY_CODE', - // Environment - 'production' or 'sandbox'. - 'environment' => 'sandbox', + // Environment - 'production' or 'sandbox'. + 'environment' => 'sandbox', - // AvaTax options - true or false - 'enableTaxCalculation' => true, - 'enableCommitting' => true, - 'enableAddressValidation' => true, - 'enablePartialRefunds' => true, - - // Enable debugging - true or false - 'debug' => true, + // AvaTax options - true or false + 'enableTaxCalculation' => true, + 'enableCommitting' => true, + 'enableAddressValidation' => false, + 'enablePartialRefunds' => true, + + // Enable debugging - true or false + 'debug' => true, + ], + + 'production' => [ + // Environment - 'production' or 'sandbox'. + 'environment' => 'production', + + // Enable debugging - true or false + 'debug' => false, + ], ]; diff --git a/src/models/Settings.php b/src/models/Settings.php index 8f233ae..b9dfcbf 100644 --- a/src/models/Settings.php +++ b/src/models/Settings.php @@ -14,6 +14,7 @@ use Craft; use craft\base\Model; +use craft\behaviors\EnvAttributeParserBehavior; /** * @author Surprise Highway @@ -161,6 +162,71 @@ class Settings extends Model // Public Methods // ========================================================================= + public function behaviors() + { + return [ + 'parser' => [ + 'class' => EnvAttributeParserBehavior::class, + 'attributes' => [ + 'accountId', + 'licenseKey', + 'companyCode', + 'sandboxAccountId', + 'sandboxLicenseKey', + 'sandboxCompanyCode', + ], + ], + ]; + } + + /** + * @return string + */ + public function getAccountId() + { + return (Craft::parseEnv($this->accountId) ?? ''); + } + + /** + * @return string + */ + public function getLicenseKey() + { + return (Craft::parseEnv($this->licenseKey) ?? ''); + } + + /** + * @return string + */ + public function getCompanyCode() + { + return (Craft::parseEnv($this->companyCode) ?? ''); + } + + /** + * @return string + */ + public function getSandboxAccountId() + { + return (Craft::parseEnv($this->sandboxAccountId) ?? ''); + } + + /** + * @return string + */ + public function getSandboxLicenseKey() + { + return (Craft::parseEnv($this->sandboxLicenseKey) ?? ''); + } + + /** + * @return string + */ + public function getSandboxCompanyCode() + { + return (Craft::parseEnv($this->sandboxCompanyCode) ?? ''); + } + /** * @inheritdoc */ @@ -253,6 +319,7 @@ public function rules() ], 'boolean' ], + ['enableAddressValidation', 'default', 'value' => '0'], ]; } } diff --git a/src/services/SalesTaxService.php b/src/services/SalesTaxService.php index 916936c..d55a544 100644 --- a/src/services/SalesTaxService.php +++ b/src/services/SalesTaxService.php @@ -237,7 +237,7 @@ public function refundFullTransaction($amount, Transaction $transaction) return true; } - Avatax::error('Transaction Code '.$transactionCode.' could not be refunded.'); + Avatax::error('Transaction Code '.$transactionCode.' could not be refunded.', ['request' => json_encode($request), 'response' => json_encode($response)]); return false; } @@ -370,8 +370,7 @@ public function validateAddress(Address $address) } // Request failed - Avatax::error('Address validation failed.'); - throw new Exception('Invalid address.'); + Avatax::error('Address validation failed.', ['request' => json_encode($address), 'response' => json_encode($response)]); return false; } @@ -430,12 +429,12 @@ private function getCompanyCode() { if($this->settings['environment'] === 'production') { - $companyCode = $this->settings['companyCode']; + $companyCode = $this->settings->getCompanyCode(); } if($this->settings['environment'] === 'sandbox') { - $companyCode = $this->settings['sandboxCompanyCode']; + $companyCode =$this->settings->getSandboxCompanyCode(); } return $companyCode; @@ -514,12 +513,15 @@ private function createClient($settings = null) if($settings['environment'] === 'production') { - if(!empty($settings['accountId']) && !empty($settings['licenseKey'])) + $accountId = (Craft::parseEnv($settings['accountId'])) ?? ''; + $licenseKey = (Craft::parseEnv($settings['licenseKey'])) ?? ''; + + if(!empty($accountId) && !empty($licenseKey)) { // Create a new client $client = new AvaTaxClient($pluginName, $pluginVersion, $machineName, 'production'); - $client->withLicenseKey( $settings['accountId'], $settings['licenseKey'] ); + $client->withLicenseKey($accountId, $licenseKey); return $client; } @@ -527,22 +529,22 @@ private function createClient($settings = null) if($settings['environment'] === 'sandbox') { - if(!empty($settings['sandboxAccountId']) && !empty($settings['sandboxLicenseKey'])) + $sandboxAccountId = (Craft::parseEnv($settings['sandboxAccountId'])) ?? ''; + $sandboxLicenseKey = (Craft::parseEnv($settings['sandboxLicenseKey'])) ?? ''; + + if(!empty($sandboxAccountId) && !empty($sandboxLicenseKey)) { // Create a new client $client = new AvaTaxClient($pluginName, $pluginVersion, $machineName, 'sandbox'); - $client->withLicenseKey( $settings['sandboxAccountId'], $settings['sandboxLicenseKey'] ); + $client->withLicenseKey($sandboxAccountId, $sandboxLicenseKey); return $client; } } // Don't have credentials - Avatax::error('Avatax Account Credentials not found'); - - // throw a craft exception which returns the error - throw new Exception('Avatax Account Credentials not found'); + Avatax::error('Avatax Account Credentials not found. Check the plugin settings.'); } /** @@ -708,10 +710,8 @@ private function getTotalTax($order, $transaction) return $response->totalTax; } - Avatax::error('Request to avatax.com failed'); - - // Request failed - throw new Exception('Request could not be completed'); + // Log error + Avatax::error('Request to avatax.com failed', ['request' => json_encode($model), 'response' => json_encode($response)]); return false; } diff --git a/src/templates/settings.twig b/src/templates/settings.twig index dc49f58..a60cabc 100644 --- a/src/templates/settings.twig +++ b/src/templates/settings.twig @@ -45,36 +45,39 @@

Production Account

- {{ forms.textField({ + {{ forms.autosuggestField({ label: 'Account ID', instructions: 'Enter your Avalara account ID here. It will be a ten-digit number (e.g. 1100012345).', id: 'accountId', name: 'accountId', value: settings['accountId'], warning: configWarning('accountId'), - errors: settings.getErrors('accountId') + errors: settings.getErrors('accountId'), + suggestEnvVars: true }) }} - {{ forms.textField({ + {{ forms.autosuggestField({ label: 'License Key', instructions: 'Enter your license key here. It will be a 16-character string (e.g. 1A2B3C4D5E6F7G8H).', id: 'licenseKey', name: 'licenseKey', value: settings['licenseKey'], warning: configWarning('licenseKey'), - errors: settings.getErrors('licenseKey') + errors: settings.getErrors('licenseKey'), + suggestEnvVars: true }) }} - {{ forms.textField({ + {{ forms.autosuggestField({ label: 'Company Code', instructions: 'Enter your [company code](https://help.avalara.com/004_AvaTax_Integrations/002_All_About_Company_Codes) here.', id: 'companyCode', name: 'companyCode', value: settings['companyCode'], warning: configWarning('companyCode'), - errors: settings.getErrors('companyCode') + errors: settings.getErrors('companyCode'), + suggestEnvVars: true }) }} @@ -82,36 +85,39 @@

Sandbox Account

- {{ forms.textField({ + {{ forms.autosuggestField({ label: 'Account ID', instructions: 'Enter your Avalara account ID here.', id: 'sandboxAccountId', name: 'sandboxAccountId', value: settings['sandboxAccountId'], warning: configWarning('sandboxAccountId'), - errors: settings.getErrors('sandboxAccountId') + errors: settings.getErrors('sandboxAccountId'), + suggestEnvVars: true }) }} - {{ forms.textField({ + {{ forms.autosuggestField({ label: 'License Key', instructions: 'Enter your license key here.', id: 'sandboxLicenseKey', name: 'sandboxLicenseKey', value: settings['sandboxLicenseKey'], warning: configWarning('sandboxLicenseKey'), - errors: settings.getErrors('sandboxLicenseKey') + errors: settings.getErrors('sandboxLicenseKey'), + suggestEnvVars: true }) }} - {{ forms.textField({ + {{ forms.autosuggestField({ label: 'Company Code', instructions: 'Enter your company code here.', id: 'sandboxCompanyCode', name: 'sandboxCompanyCode', value: settings['sandboxCompanyCode'], warning: configWarning('sandboxCompanyCode'), - errors: settings.getErrors('sandboxCompanyCode') + errors: settings.getErrors('sandboxCompanyCode'), + suggestEnvVars: true }) }}