@medusajs/tax
Version:
Medusa Tax module
452 lines • 19.7 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("@medusajs/framework/types");
const utils_1 = require("@medusajs/framework/utils");
const _models_1 = require("../models");
const generateForModels = { TaxRate: _models_1.TaxRate, TaxRegion: _models_1.TaxRegion, TaxRateRule: _models_1.TaxRateRule, TaxProvider: _models_1.TaxProvider };
class TaxModuleService extends utils_1.ModulesSdkUtils.MedusaService(generateForModels) {
constructor({ baseRepository, taxRateService, taxRegionService, taxRateRuleService, taxProviderService, }, moduleDeclaration) {
// @ts-ignore
super(...arguments);
this.moduleDeclaration = moduleDeclaration;
this.container_ = arguments[0];
this.baseRepository_ = baseRepository;
this.taxRateService_ = taxRateService;
this.taxRegionService_ = taxRegionService;
this.taxRateRuleService_ = taxRateRuleService;
this.taxProviderService_ = taxProviderService;
}
// @ts-expect-error
async createTaxRates(data, sharedContext = {}) {
const input = Array.isArray(data) ? data : [data];
const rates = await this.createTaxRates_(input, sharedContext);
return Array.isArray(data) ? rates : rates[0];
}
async createTaxRates_(data, sharedContext = {}) {
const [rules, rateData] = data.reduce((acc, region) => {
const { rules, ...rest } = region;
acc[0].push(rules);
acc[1].push(rest);
return acc;
}, [[], []]);
const rates = await this.taxRateService_.create(rateData, sharedContext);
const rulesToCreate = rates
.reduce((acc, rate, i) => {
const rateRules = rules[i];
if ((0, utils_1.isDefined)(rateRules)) {
acc.push(rateRules.map((r) => {
return {
...r,
created_by: rate.created_by,
tax_rate_id: rate.id,
};
}));
}
return acc;
}, [])
.flat();
if (rulesToCreate.length > 0) {
await this.taxRateRuleService_.create(rulesToCreate, sharedContext);
}
return await this.baseRepository_.serialize(rates, {
populate: true,
});
}
// @ts-expect-error
async updateTaxRates(selector, data, sharedContext = {}) {
const rates = await this.updateTaxRates_(selector, data, sharedContext);
const serialized = await this.baseRepository_.serialize(rates, { populate: true });
return (0, utils_1.isString)(selector) ? serialized[0] : serialized;
}
async updateTaxRates_(idOrSelector, data, sharedContext = {}) {
const selector = Array.isArray(idOrSelector) || (0, utils_1.isString)(idOrSelector)
? { id: idOrSelector }
: idOrSelector;
if (data.rules) {
await this.setTaxRateRulesForTaxRates(idOrSelector, data.rules, data.updated_by, sharedContext);
delete data.rules;
}
return await this.taxRateService_.update({ selector, data }, sharedContext);
}
async setTaxRateRulesForTaxRates(idOrSelector, rules, createdBy, sharedContext = {}) {
const selector = Array.isArray(idOrSelector) || (0, utils_1.isString)(idOrSelector)
? { id: idOrSelector }
: idOrSelector;
await this.taxRateRuleService_.softDelete({ tax_rate: selector }, sharedContext);
// TODO: this is a temporary solution seems like mikro-orm doesn't persist
// the soft delete which results in the creation below breaking the unique
// constraint
await this.taxRateRuleService_.list({ tax_rate: selector }, { select: ["id"] }, sharedContext);
if (rules.length === 0) {
return;
}
const rateIds = await this.getTaxRateIdsFromSelector(idOrSelector);
const toCreate = rateIds
.map((id) => {
return rules.map((r) => {
return {
...r,
created_by: createdBy,
tax_rate_id: id,
};
});
})
.flat();
return await this.createTaxRateRules(toCreate, sharedContext);
}
async getTaxRateIdsFromSelector(idOrSelector, sharedContext = {}) {
if (Array.isArray(idOrSelector)) {
return idOrSelector;
}
if ((0, utils_1.isString)(idOrSelector)) {
return [idOrSelector];
}
const rates = await this.taxRateService_.list(idOrSelector, { select: ["id"] }, sharedContext);
return rates.map((r) => r.id);
}
async upsertTaxRates(data, sharedContext = {}) {
const result = await this.taxRateService_.upsert(data, sharedContext);
const serialized = await this.baseRepository_.serialize(result, { populate: true });
return Array.isArray(data) ? serialized : serialized[0];
}
// @ts-expect-error
async createTaxRegions(data, sharedContext = {}) {
const input = Array.isArray(data) ? data : [data];
const result = await this.createTaxRegions_(input, sharedContext);
return Array.isArray(data) ? result : result[0];
}
async createTaxRegions_(data, sharedContext = {}) {
const { defaultRates, regionData } = this.prepareTaxRegionInputForCreate(data);
await this.verifyProvinceToCountryMatch(regionData, sharedContext);
const regions = await this.taxRegionService_.create(regionData, sharedContext);
const rates = regions
.map((region, i) => {
if (!defaultRates[i]) {
return false;
}
return {
...defaultRates[i],
tax_region_id: region.id,
};
})
.filter(Boolean);
if (rates.length !== 0) {
await this.createTaxRates(rates, sharedContext);
}
return await this.baseRepository_.serialize(regions, { populate: true });
}
// @ts-expect-error
async createTaxRateRules(data, sharedContext = {}) {
const input = Array.isArray(data) ? data : [data];
const result = await this.createTaxRateRules_(input, sharedContext);
return Array.isArray(data) ? result : result[0];
}
async createTaxRateRules_(data, sharedContext = {}) {
const rules = await this.taxRateRuleService_.create(data, sharedContext);
return await this.baseRepository_.serialize(rules, {
populate: true,
});
}
async getTaxLines(items, calculationContext, sharedContext = {}) {
const normalizedContext = this.normalizeTaxCalculationContext(calculationContext);
const regions = await this.taxRegionService_.list({
$or: [
{
country_code: normalizedContext.address.country_code,
province_code: null,
},
{
country_code: normalizedContext.address.country_code,
province_code: normalizedContext.address.province_code,
},
],
}, {}, sharedContext);
const parentRegion = regions.find((r) => r.province_code === null);
if (!parentRegion) {
return [];
}
const toReturn = await (0, utils_1.promiseAll)(items.map(async (item) => {
const regionIds = regions.map((r) => r.id);
const rateQuery = this.getTaxRateQueryForItem(item, regionIds);
const candidateRates = await this.taxRateService_.list(rateQuery, {
relations: ["tax_region", "rules"],
}, sharedContext);
const applicableRates = await this.getTaxRatesForItem(item, candidateRates);
return {
rates: applicableRates,
item,
};
}));
const taxLines = await this.getTaxLinesFromProvider(parentRegion.provider_id, toReturn, calculationContext);
return taxLines;
}
async getTaxLinesFromProvider(providerId, items, calculationContext) {
const provider = this.taxProviderService_.retrieveProvider(providerId);
const [itemLines, shippingLines] = items.reduce((acc, line) => {
if ("shipping_option_id" in line.item) {
acc[1].push({
shipping_line: line.item,
rates: line.rates,
});
}
else {
acc[0].push({
line_item: line.item,
rates: line.rates,
});
}
return acc;
}, [[], []]);
const itemTaxLines = await provider.getTaxLines(itemLines, shippingLines, calculationContext);
return itemTaxLines;
}
normalizeTaxCalculationContext(context) {
return {
...context,
address: {
...context.address,
country_code: this.normalizeRegionCodes(context.address.country_code),
province_code: context.address.province_code
? this.normalizeRegionCodes(context.address.province_code)
: null,
},
};
}
prepareTaxRegionInputForCreate(data) {
const regionsWithDefaultRate = Array.isArray(data) ? data : [data];
const defaultRates = [];
const regionData = [];
for (const region of regionsWithDefaultRate) {
const { default_tax_rate, ...rest } = region;
if (!default_tax_rate) {
defaultRates.push(null);
}
else {
defaultRates.push({
...default_tax_rate,
is_default: true,
created_by: region.created_by,
});
}
regionData.push({
...rest,
province_code: rest.province_code
? this.normalizeRegionCodes(rest.province_code)
: null,
country_code: this.normalizeRegionCodes(rest.country_code),
});
}
return { defaultRates, regionData };
}
async verifyProvinceToCountryMatch(regionsToVerify, sharedContext = {}) {
const parentIds = regionsToVerify.map((i) => i.parent_id).filter(utils_1.isDefined);
if (parentIds.length > 0) {
const parentRegions = await this.taxRegionService_.list({ id: { $in: parentIds } }, { select: ["id", "country_code"] }, sharedContext);
for (const region of regionsToVerify) {
if ((0, utils_1.isDefined)(region.parent_id)) {
const parentRegion = parentRegions.find((r) => r.id === region.parent_id);
if (!(0, utils_1.isDefined)(parentRegion)) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Province region must belong to a parent region. You are trying to create a province region with (country: ${region.country_code}, province: ${region.province_code}) but parent does not exist`);
}
if (parentRegion.country_code !== region.country_code) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Province region must belong to a parent region with the same country code. You are trying to create a province region with (country: ${region.country_code}, province: ${region.province_code}) but parent expects (country: ${parentRegion.country_code})`);
}
}
}
}
}
async getTaxRatesForItem(item, rates) {
if (!rates.length) {
return [];
}
const prioritizedRates = this.prioritizeRates(rates, item);
const rate = prioritizedRates[0];
const ratesToReturn = [rate];
// If the rate can be combined we need to find the rate's
// parent region and add that rate too. If not we can return now.
if (!(rate.is_combinable && rate.tax_region.parent_id)) {
return ratesToReturn;
}
// First parent region rate in prioritized rates
// will be the most granular rate.
const parentRate = prioritizedRates.find((r) => r.tax_region.id === rate.tax_region.parent_id);
if (parentRate) {
ratesToReturn.push(parentRate);
}
return ratesToReturn;
}
getTaxRateQueryForItem(item, regionIds) {
const isShipping = "shipping_option_id" in item;
let ruleQuery = isShipping
? [
{
reference: "shipping_option",
reference_id: item.shipping_option_id,
},
]
: [
{
reference: "product",
reference_id: item.product_id,
},
{
reference: "product_type",
reference_id: item.product_type_id,
},
];
return {
$and: [
{ tax_region_id: regionIds },
{ $or: [{ is_default: true }, { rules: { $or: ruleQuery } }] },
],
};
}
checkRuleMatches(rate, item) {
if (rate.rules.length === 0) {
return {
isProductMatch: false,
isProductTypeMatch: false,
isShippingMatch: false,
};
}
let isProductMatch = false;
const isShipping = "shipping_option_id" in item;
const matchingRules = rate.rules.filter((rule) => {
if (isShipping) {
return (rule.reference === "shipping" &&
rule.reference_id === item.shipping_option_id);
}
return ((rule.reference === "product" &&
rule.reference_id === item.product_id) ||
(rule.reference === "product_type" &&
rule.reference_id === item.product_type_id));
});
if (matchingRules.some((rule) => rule.reference === "product")) {
isProductMatch = true;
}
return {
isProductMatch,
isProductTypeMatch: matchingRules.length > 0,
isShippingMatch: isShipping && matchingRules.length > 0,
};
}
prioritizeRates(rates, item) {
const decoratedRates = rates.map((rate) => {
const { isProductMatch, isProductTypeMatch, isShippingMatch } = this.checkRuleMatches(rate, item);
const isProvince = rate.tax_region.province_code !== null;
const isDefault = rate.is_default;
const decoratedRate = {
...rate,
priority_score: 7,
};
if ((isShippingMatch || isProductMatch) && isProvince) {
decoratedRate.priority_score = 1;
}
else if (isProductTypeMatch && isProvince) {
decoratedRate.priority_score = 2;
}
else if (isDefault && isProvince) {
decoratedRate.priority_score = 3;
}
else if ((isShippingMatch || isProductMatch) && !isProvince) {
decoratedRate.priority_score = 4;
}
else if (isProductTypeMatch && !isProvince) {
decoratedRate.priority_score = 5;
}
else if (isDefault && !isProvince) {
decoratedRate.priority_score = 6;
}
return decoratedRate;
});
return decoratedRates.sort((a, b) => a.priority_score - b.priority_score);
}
normalizeRegionCodes(code) {
return code.toLowerCase();
}
}
exports.default = TaxModuleService;
__decorate([
(0, utils_1.InjectManager)()
// @ts-expect-error
,
__param(1, (0, utils_1.MedusaContext)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], TaxModuleService.prototype, "createTaxRates", null);
__decorate([
(0, utils_1.InjectTransactionManager)(),
__param(1, (0, utils_1.MedusaContext)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array, Object]),
__metadata("design:returntype", Promise)
], TaxModuleService.prototype, "createTaxRates_", null);
__decorate([
(0, utils_1.InjectManager)()
// @ts-expect-error
,
__param(2, (0, utils_1.MedusaContext)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object, Object]),
__metadata("design:returntype", Promise)
], TaxModuleService.prototype, "updateTaxRates", null);
__decorate([
(0, utils_1.InjectTransactionManager)(),
__param(2, (0, utils_1.MedusaContext)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object, Object]),
__metadata("design:returntype", Promise)
], TaxModuleService.prototype, "updateTaxRates_", null);
__decorate([
(0, utils_1.InjectTransactionManager)(),
__param(1, (0, utils_1.MedusaContext)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], TaxModuleService.prototype, "upsertTaxRates", null);
__decorate([
(0, utils_1.InjectManager)()
// @ts-expect-error
,
__param(1, (0, utils_1.MedusaContext)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], TaxModuleService.prototype, "createTaxRegions", null);
__decorate([
(0, utils_1.InjectManager)()
// @ts-expect-error
,
__param(1, (0, utils_1.MedusaContext)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], TaxModuleService.prototype, "createTaxRateRules", null);
__decorate([
(0, utils_1.InjectTransactionManager)(),
__param(1, (0, utils_1.MedusaContext)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array, Object]),
__metadata("design:returntype", Promise)
], TaxModuleService.prototype, "createTaxRateRules_", null);
__decorate([
(0, utils_1.InjectManager)(),
__param(2, (0, utils_1.MedusaContext)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Array, Object, Object]),
__metadata("design:returntype", Promise)
], TaxModuleService.prototype, "getTaxLines", null);
//# sourceMappingURL=tax-module-service.js.map
;