Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions imports/plugins/core/taxes/client/components/GeneralTaxSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export default class GeneralTaxSettings extends Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired,
settingsDoc: PropTypes.shape({
activeTaxServiceName: PropTypes.string,
primaryTaxServiceName: PropTypes.string,
fallbackTaxServiceName: PropTypes.string,
defaultTaxCode: PropTypes.string
}),
taxServices: PropTypes.arrayOf(PropTypes.shape({
Expand All @@ -31,6 +32,11 @@ export default class GeneralTaxSettings extends Component {
}
};

enableFallbackTaxSelect = (currentSettings) => {
if (currentSettings.primaryTaxServiceName) return false;
return true;
}

get taxServicesOptions() {
const { taxServices } = this.props;

Expand All @@ -47,15 +53,26 @@ export default class GeneralTaxSettings extends Component {
render() {
const { onSubmit, settingsDoc, validator } = this.props;

const activeTaxServiceNameInputId = `activeTaxServiceName_${this.uniqueInstanceIdentifier}`;
const primaryTaxServiceNameInputId = `primaryTaxServiceName_${this.uniqueInstanceIdentifier}`;
const fallbackTaxServiceNameInputId = `fallbackTaxServiceName_${this.uniqueInstanceIdentifier}`;
const defaultTaxCodeInputId = `defaultTaxCode_${this.uniqueInstanceIdentifier}`;

return (
<div className="clearfix">
<Form ref={(formRef) => { this.form = formRef; }} onSubmit={onSubmit} validator={validator} value={settingsDoc}>
<Field name="activeTaxServiceName" label={i18next.t("admin.taxSettings.activeTaxServiceName")} labelFor={activeTaxServiceNameInputId}>
<Select id={activeTaxServiceNameInputId} name="activeTaxServiceName" options={this.taxServicesOptions} placeholder={i18next.t("admin.taxSettings.activeTaxServiceNamePlaceholder")} />
<ErrorsBlock names={["activeTaxServiceName"]} />
<Field name="primaryTaxServiceName" label={i18next.t("admin.taxSettings.primaryTaxServiceName")} labelFor={primaryTaxServiceNameInputId}>
<Select id={primaryTaxServiceNameInputId} name="primaryTaxServiceName" options={this.taxServicesOptions} placeholder={i18next.t("admin.taxSettings.primaryTaxServiceNamePlaceholder")} />
<ErrorsBlock names={["primaryTaxServiceName"]} />
</Field>
<Field name="fallbackTaxServiceName" label={i18next.t("admin.taxSettings.fallbackTaxServiceName")} labelFor={fallbackTaxServiceNameInputId}>
<Select
id={fallbackTaxServiceNameInputId}
name="fallbackTaxServiceName"
options={this.taxServicesOptions}
placeholder={i18next.t("admin.taxSettings.fallbackTaxServiceNamePlaceholder")}
isReadOnly={this.enableFallbackTaxSelect}
/>
<ErrorsBlock names={["fallbackTaxServiceName"]} />
</Field>
<Field name="defaultTaxCode" label={i18next.t("admin.taxSettings.defaultTaxCode")} labelFor={defaultTaxCodeInputId}>
<TextInput id={defaultTaxCodeInputId} name="defaultTaxCode" />
Expand Down
8 changes: 8 additions & 0 deletions imports/plugins/core/taxes/lib/collections/schemas/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ export const TaxPackageConfig = PackageConfig.clone().extend({
blackbox: false,
defaultValue: {}
},
"settings.primaryTaxServiceName": {
type: String,
optional: true
},
"settings.fallbackTaxServiceName": {
type: String,
optional: true
},
"settings.defaultTaxCode": {
type: String,
optional: true
Expand Down
6 changes: 4 additions & 2 deletions imports/plugins/core/taxes/server/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@
"nottaxable": "Not Taxable",
"confirmRateDelete": "Confirm tax rate deletion",
"defaultTaxCode": "Default tax code for products",
"activeTaxServiceName": "Active tax service",
"activeTaxServiceNamePlaceholder": "None (do not charge tax)"
"primaryTaxServiceName": "Primary tax service",
"primaryTaxServiceNamePlaceholder": "None (do not charge tax)",
"fallbackTaxServiceName": "Fallback tax service",
"fallbackTaxServiceNamePlaceholder": "None (no fallback tax service)"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Logger from "@reactioncommerce/logger";
import ReactionError from "@reactioncommerce/reaction-error";
import { CommonOrder } from "/imports/plugins/core/orders/server/no-meteor/simpleSchemas";
import { getActiveTaxServiceForShop } from "../registration";
import { getTaxServicesForShop } from "../registration";
import { TaxServiceResult } from "../../../lib/simpleSchemas";

/**
Expand All @@ -26,7 +26,7 @@ export default async function getFulfillmentGroupTaxes(context, { order, forceZe

const { items, shopId } = order;

const activeTaxService = await getActiveTaxServiceForShop(context, shopId);
const { primaryTaxService, fallbackTaxService } = await getTaxServicesForShop(context, shopId);

const defaultReturnValue = {
taxSummary: {
Expand All @@ -38,27 +38,39 @@ export default async function getFulfillmentGroupTaxes(context, { order, forceZe
itemTaxes: items.map((item) => ({ itemId: item._id, tax: 0, taxableAmount: 0, taxes: [] }))
};

if (!activeTaxService) {
if (!primaryTaxService) {
return forceZeroes ? defaultReturnValue : { itemTaxes: [], taxSummary: null };
}

let taxServiceResult;
try {
taxServiceResult = await activeTaxService.functions.calculateOrderTaxes({ context, order });
taxServiceResult = await primaryTaxService.functions.calculateOrderTaxes({ context, order });
} catch (error) {
Logger.error(`Error in calculateOrderTaxes for the active tax service (${activeTaxService.displayName})`, error);
Logger.error(`Error in calculateOrderTaxes for the primary tax service (${primaryTaxService.displayName})`, error);
throw new ReactionError("internal-error", "Error while calculating taxes");
}

// The tax service may return `null` if it can't calculate due to missing info
if (!taxServiceResult && fallbackTaxService) {
// if primaryTaxService returns null, try the fallbackTaxService before falling back to forceZeroTax (if set)
Logger.info("Primary tax service calculation returned null. Using set fallback tax service");
try {
taxServiceResult = await fallbackTaxService.functions.calculateOrderTaxes({ context, order });
} catch (fallbackError) {
Logger.error(`Error in calculateOrderTaxes for the fallback tax service (${fallbackTaxService.displayName})`, fallbackError);
throw new ReactionError("internal-error", "Error while calculating taxes");
}
}

// if none of primary and fallback services returns valid tax response, default to zero or empty
if (!taxServiceResult) {
return forceZeroes ? defaultReturnValue : { itemTaxes: [], taxSummary: null };
}

try {
TaxServiceResult.validate(taxServiceResult);
} catch (error) {
Logger.error(`Invalid return from calculateOrderTaxes for the active tax service (${activeTaxService.displayName})`, error);
Logger.error("Invalid return from the calculateOrderTaxes function", error);
throw new ReactionError("internal-error", "Error while calculating taxes");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sortBy } from "lodash";
import ReactionError from "@reactioncommerce/reaction-error";
import { getActiveTaxServiceForShop } from "../registration";
import { getTaxServicesForShop } from "../registration";

/**
* @name taxCodes
Expand All @@ -16,9 +16,9 @@ export default async function taxCodes(context, shopId) {
throw new ReactionError("access-denied", "Access denied");
}

const activeTaxService = await getActiveTaxServiceForShop(context, shopId);
if (!activeTaxService) return [];
const { primaryTaxService } = await getTaxServicesForShop(context, shopId);
if (!primaryTaxService) return [];

const list = await activeTaxService.functions.getTaxCodes({ context, shopId });
const list = await primaryTaxService.functions.getTaxCodes({ context, shopId });
return sortBy(list, "label");
}
27 changes: 17 additions & 10 deletions imports/plugins/core/taxes/server/no-meteor/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,28 @@ export function registerPluginHandler({ name: pluginName, taxServices: pluginTax
/**
* @param {Object} context The app context
* @param {String} shopId The shop ID
* @returns {Object|null} The definition from registerPackage for the tax service that is
* currently enabled for the shop with ID `shopId`
* @returns {Object} An object containing the definitions from registerPackage for the
* primary and fallback tax services currently enabled for the shop with ID `shopId`.
*/
export async function getActiveTaxServiceForShop(context, shopId) {
export async function getTaxServicesForShop(context, shopId) {
const plugin = await context.collections.Packages.findOne({ name: "reaction-taxes", shopId });
if (!plugin) return null;
if (!plugin) return {};

const { activeTaxServiceName } = plugin.settings || {};
if (!activeTaxServiceName) return null;
const { primaryTaxServiceName, fallbackTaxServiceName } = plugin.settings || {};
if (!primaryTaxServiceName) return {}; // at least a primary service must be set

const config = taxServices[activeTaxServiceName];
if (!config) {
throw new Error(`Active tax service is "${activeTaxServiceName}" but no such service exists. ` +
const primaryTaxService = taxServices[primaryTaxServiceName];
const fallbackTaxService = taxServices[fallbackTaxServiceName];

if (!primaryTaxService) {
throw new Error(`Primary tax service is "${primaryTaxServiceName}" but no such service exists. ` +
"Did you forget to install the plugin that provides this service?");
}

if (fallbackTaxServiceName && !fallbackTaxService) {
throw new Error(`Fallback tax service is "${fallbackTaxServiceName}" but no such service exists. ` +
"Did you forget to install the plugin that provides this service?");
}

return config;
return { primaryTaxService, fallbackTaxService };
}
72 changes: 72 additions & 0 deletions imports/plugins/core/taxes/server/no-meteor/registration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { cloneDeep } from "lodash";
import mockContext from "/imports/test-utils/helpers/mockContext";
import { registerPluginHandler, getTaxServicesForShop } from "./registration";

const fakeShopId = "FAKE_SHOP_ID";
const fakePackage = {
_id: "FAKE_PKG_ID",
name: "fake",
shopId: fakeShopId,
settings: {
primaryTaxServiceName: "custom-rates",
fallbackTaxServiceName: "rapid-tax-service"
}
};

// test pluginTaxServices
const pluginTaxServices = [{
name: "rapid-tax-service",
taxServices: [{
displayName: "RapidTaxService",
name: "rapid-tax-service",
functions: {
calculateOrderTaxes() { },
getTaxCodes() { }
}
}]
}, {
name: "custom-rates",
taxServices: [{
displayName: "Custom Rates",
name: "custom-rates",
functions: {
calculateOrderTaxes() { },
getTaxCodes() { }
}
}]
}];

pluginTaxServices.forEach(({ name, taxServices }) => {
registerPluginHandler({ name, taxServices });
});

test("returns object containing primary tax service when available", async () => {
const testPackage = cloneDeep(fakePackage);
testPackage.settings.fallbackTaxServiceName = null;
mockContext.collections.Packages.findOne.mockReturnValueOnce(testPackage);
const result = await getTaxServicesForShop(mockContext, fakeShopId);

expect(mockContext.collections.Packages.findOne).toHaveBeenCalledWith({ name: "reaction-taxes", shopId: fakeShopId });
expect(typeof result.primaryTaxService).toEqual("object");
});

test("returns an empty object when no primary tax service is found", async () => {
const testPackage = cloneDeep(fakePackage);
testPackage.settings = null;
mockContext.collections.Packages.findOne.mockReturnValueOnce(testPackage);
const result = await getTaxServicesForShop(mockContext, fakeShopId);

expect(mockContext.collections.Packages.findOne).toHaveBeenCalledWith({ name: "reaction-taxes", shopId: fakeShopId });
expect(result.primaryTaxService).toEqual(undefined);
expect(result.fallbackTaxService).toEqual(undefined);
});

test("returns object containing both primary and fallback tax service when both are available", async () => {
const testPackage = cloneDeep(fakePackage);
mockContext.collections.Packages.findOne.mockReturnValueOnce(testPackage);
const result = await getTaxServicesForShop(mockContext, fakeShopId);

expect(mockContext.collections.Packages.findOne).toHaveBeenCalledWith({ name: "reaction-taxes", shopId: fakeShopId });
expect(typeof result.primaryTaxService).toEqual("object");
expect(typeof result.fallbackTaxService).toEqual("object");
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Migrations } from "meteor/percolate:migrations";
import { Packages } from "/lib/collections";

Migrations.add({
version: 52,
up() {
Packages.update({
name: "reaction-taxes"
}, {
$rename: { "settings.activeTaxServiceName": "settings.primaryTaxServiceName" }
}, {
multi: true
});
},
down() {
Packages.update({
name: "reaction-taxes"
}, {
$rename: { "settings.primaryTaxServiceName": "settings.activeTaxServiceName" }
}, {
multi: true
});
}
});
1 change: 1 addition & 0 deletions imports/plugins/core/versions/server/migrations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ import "./48_catalog_variant_inventory";
import "./49_update_idp_route_name_to_match_pattern";
import "./50_create_default_navigation_trees";
import "./51_catalog_variant_inventory";
import "./52_change_activetaxfield_to_primary";