Initial commit: backend, storefront, vendor-panel added
This commit is contained in:
45
backend/packages/modules/stripe-tax-provider/package.json
Normal file
45
backend/packages/modules/stripe-tax-provider/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/stripe-tax-provider",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/__tests__",
|
||||
"!dist/**/__mocks__",
|
||||
"!dist/**/__fixtures__"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "rimraf dist && tsc --build",
|
||||
"migration:initial": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial",
|
||||
"migration:create": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create",
|
||||
"migration:up": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up",
|
||||
"orm:cache:clear": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@medusajs/framework": "2.8.6",
|
||||
"@medusajs/test-utils": "2.8.6",
|
||||
"@mercurjs/framework": "*",
|
||||
"@mikro-orm/cli": "6.4.3",
|
||||
"@mikro-orm/core": "6.4.3",
|
||||
"@mikro-orm/migrations": "6.4.3",
|
||||
"@mikro-orm/postgresql": "6.4.3",
|
||||
"@swc/core": "^1.7.28",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"jest": "^29.7.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"tsc-alias": "^1.8.6",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@medusajs/framework": "2.8.6",
|
||||
"@mikro-orm/core": "6.4.3",
|
||||
"@mikro-orm/migrations": "6.4.3",
|
||||
"@mikro-orm/postgresql": "6.4.3",
|
||||
"awilix": "^8.0.1"
|
||||
}
|
||||
}
|
||||
48
backend/packages/modules/stripe-tax-provider/src/client.ts
Normal file
48
backend/packages/modules/stripe-tax-provider/src/client.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import Stripe from 'stripe'
|
||||
|
||||
import { MedusaError } from '@medusajs/framework/utils'
|
||||
|
||||
import {
|
||||
StripeTaxCalculationResponse,
|
||||
StripeTaxCalculationResponseValidator
|
||||
} from './validators'
|
||||
|
||||
export default class StripeTaxClient {
|
||||
private stripe_: Stripe
|
||||
constructor(apiKey: string = 'sk_') {
|
||||
this.stripe_ = new Stripe(apiKey)
|
||||
}
|
||||
|
||||
async getCalculation(
|
||||
payload: Stripe.Tax.CalculationCreateParams
|
||||
): Promise<StripeTaxCalculationResponse> {
|
||||
const response = await this.stripe_.tax.calculations.create(payload)
|
||||
|
||||
if (response.line_items && response.line_items.has_more) {
|
||||
let lastItems = response.line_items
|
||||
while (lastItems.has_more) {
|
||||
const lastId = lastItems.data.pop()!.id
|
||||
const currentItems = await this.stripe_.tax.calculations.listLineItems(
|
||||
response.id!,
|
||||
{
|
||||
limit: 100,
|
||||
starting_after: lastId,
|
||||
expand: ['line_items.data.tax_breakdown']
|
||||
}
|
||||
)
|
||||
response.line_items.data.push(...currentItems.data)
|
||||
lastItems = currentItems
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const calculation = StripeTaxCalculationResponseValidator.parse(response)
|
||||
return calculation
|
||||
} catch {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
'Incorrect Stripe tax calculation response'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Modules } from "@medusajs/framework/utils";
|
||||
import { ModuleProvider } from "@medusajs/utils";
|
||||
|
||||
import StripeTaxProvider from "./service";
|
||||
export { StripeTaxProvider };
|
||||
|
||||
export default ModuleProvider(Modules.TAX, {
|
||||
services: [StripeTaxProvider],
|
||||
});
|
||||
142
backend/packages/modules/stripe-tax-provider/src/service.ts
Normal file
142
backend/packages/modules/stripe-tax-provider/src/service.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import Stripe from "stripe";
|
||||
|
||||
import {
|
||||
ITaxProvider,
|
||||
RemoteQueryFunction,
|
||||
TaxTypes,
|
||||
} from "@medusajs/framework/types";
|
||||
import { MathBN } from "@medusajs/framework/utils";
|
||||
import { Logger } from "@medusajs/medusa";
|
||||
|
||||
import { getSmallestUnit } from "@mercurjs/framework";
|
||||
import StripeTaxClient from "./client";
|
||||
import { StripeTaxCalculationResponseValidator } from "./validators";
|
||||
|
||||
type InjectedDependencies = {
|
||||
logger: Logger;
|
||||
remoteQuery: Omit<RemoteQueryFunction, symbol>;
|
||||
};
|
||||
|
||||
type Options = {
|
||||
apiKey: string;
|
||||
defaultTaxcode: string;
|
||||
};
|
||||
|
||||
export default class StripeTaxProvider implements ITaxProvider {
|
||||
static identifier = "stripe-tax-provider";
|
||||
|
||||
private readonly client_: StripeTaxClient;
|
||||
private defaultTaxcode_: string;
|
||||
|
||||
private remoteQuery_: Omit<RemoteQueryFunction, symbol>;
|
||||
|
||||
constructor({ remoteQuery }: InjectedDependencies, options: Options) {
|
||||
this.defaultTaxcode_ = options.defaultTaxcode;
|
||||
this.client_ = new StripeTaxClient(options.apiKey);
|
||||
this.remoteQuery_ = remoteQuery;
|
||||
}
|
||||
|
||||
getIdentifier(): string {
|
||||
return StripeTaxProvider.identifier;
|
||||
}
|
||||
|
||||
async getTaxLines(
|
||||
itemLines: TaxTypes.ItemTaxCalculationLine[],
|
||||
shippingLines: TaxTypes.ShippingTaxCalculationLine[],
|
||||
{ address }: TaxTypes.TaxCalculationContext
|
||||
): Promise<(TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO)[]> {
|
||||
if (itemLines.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currency =
|
||||
itemLines[0].line_item.currency_code?.toLowerCase() || "eur";
|
||||
|
||||
const shipping = shippingLines.reduce((acc, l) => {
|
||||
return (acc = acc.plus(MathBN.convert(l.shipping_line.unit_price || 0)));
|
||||
}, MathBN.convert(0));
|
||||
|
||||
const line_items: Stripe.Tax.CalculationCreateParams.LineItem[] = [];
|
||||
for (const item of itemLines) {
|
||||
const tax_code = await this.getProductTaxCode_(item.line_item.product_id);
|
||||
|
||||
const quantity = MathBN.convert(item.line_item.quantity || 0);
|
||||
const amount = MathBN.convert(
|
||||
item.line_item.unit_price || 0
|
||||
).multipliedBy(quantity);
|
||||
|
||||
line_items.push({
|
||||
reference: item.line_item.id,
|
||||
amount: getSmallestUnit(amount, currency),
|
||||
quantity: quantity.toNumber(),
|
||||
tax_code,
|
||||
});
|
||||
}
|
||||
|
||||
const calculationResponse = await this.client_.getCalculation({
|
||||
currency,
|
||||
customer_details: {
|
||||
address: {
|
||||
country: address.country_code,
|
||||
city: address.city,
|
||||
line1: address.address_1,
|
||||
line2: address.address_2,
|
||||
postal_code: address.postal_code,
|
||||
state: address.province_code,
|
||||
},
|
||||
},
|
||||
shipping_cost: { amount: getSmallestUnit(shipping, currency) },
|
||||
line_items,
|
||||
expand: ["line_items.data.tax_breakdown"],
|
||||
});
|
||||
|
||||
const calculation =
|
||||
StripeTaxCalculationResponseValidator.parse(calculationResponse);
|
||||
|
||||
const itemTaxLines: TaxTypes.ItemTaxLineDTO[] = calculation.line_items
|
||||
? calculation.line_items?.data.map((item) => {
|
||||
return {
|
||||
line_item_id: item.reference!,
|
||||
rate: MathBN.convert(
|
||||
item.tax_breakdown[0].tax_rate_details[0].percentage_decimal || 0
|
||||
).toNumber(),
|
||||
code: item.tax_code,
|
||||
provider_id: this.getIdentifier(),
|
||||
name: `Stripe-${item.tax_code}`,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const shippingTaxLines: TaxTypes.ShippingTaxLineDTO[] = shippingLines.map(
|
||||
(i) => {
|
||||
return {
|
||||
shipping_line_id: i.shipping_line.id,
|
||||
code: "SHIPPING",
|
||||
name: "SHIPPING",
|
||||
provider_id: this.getIdentifier(),
|
||||
rate: MathBN.convert(
|
||||
calculation.shipping_cost.tax_breakdown[0].tax_rate_details[0]
|
||||
.percentage_decimal || 0
|
||||
).toNumber(),
|
||||
};
|
||||
}
|
||||
);
|
||||
return [...itemTaxLines, ...shippingTaxLines];
|
||||
}
|
||||
|
||||
private async getProductTaxCode_(productId: string) {
|
||||
const {
|
||||
data: [product],
|
||||
} = await this.remoteQuery_.graph({
|
||||
entity: "product",
|
||||
fields: ["categories.tax_code.code"],
|
||||
filters: { id: productId },
|
||||
});
|
||||
|
||||
if (!product || product.categories.length !== 1) {
|
||||
return this.defaultTaxcode_;
|
||||
}
|
||||
|
||||
return product.categories[0].tax_code?.code || this.defaultTaxcode_;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const TaxBreakdownObject = z.object({
|
||||
amount: z.number(),
|
||||
taxable_amount: z.number(),
|
||||
sourcing: z.string(),
|
||||
tax_rate_details: z.array(
|
||||
z.object({
|
||||
display_name: z.string(),
|
||||
percentage_decimal: z.string(),
|
||||
tax_type: z.string()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
export const StripeTaxCalculationResponseValidator = z.object({
|
||||
id: z.string(),
|
||||
line_items: z.object({
|
||||
has_more: z.boolean(),
|
||||
total_count: z.number(),
|
||||
data: z.array(
|
||||
z.object({
|
||||
reference: z.string(),
|
||||
amount: z.number(),
|
||||
amount_tax: z.number(),
|
||||
tax_behavior: z.string(),
|
||||
tax_breakdown: z.array(TaxBreakdownObject),
|
||||
tax_code: z.string(),
|
||||
quantity: z.number()
|
||||
})
|
||||
)
|
||||
}),
|
||||
shipping_cost: z.object({
|
||||
amount: z.number(),
|
||||
amount_tax: z.number(),
|
||||
tax_code: z.string(),
|
||||
tax_behavior: z.string(),
|
||||
tax_breakdown: z.array(TaxBreakdownObject)
|
||||
})
|
||||
})
|
||||
|
||||
export type StripeTaxCalculationResponse = z.infer<
|
||||
typeof StripeTaxCalculationResponseValidator
|
||||
>
|
||||
27
backend/packages/modules/stripe-tax-provider/tsconfig.json
Normal file
27
backend/packages/modules/stripe-tax-provider/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2021"],
|
||||
"target": "ES2021",
|
||||
"outDir": "${configDir}/dist",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noUnusedLocals": true,
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitReturns": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"incremental": false
|
||||
},
|
||||
"include": ["${configDir}/src"],
|
||||
"exclude": ["${configDir}/dist", "${configDir}/node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user