Initial commit: backend, storefront, vendor-panel added

This commit is contained in:
2025-08-01 11:05:32 +08:00
commit 08174125d2
2958 changed files with 310810 additions and 0 deletions

View 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"
}
}

View 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'
)
}
}
}

View File

@@ -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],
});

View 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_;
}
}

View File

@@ -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
>

View 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"]
}