Initial commit: backend, storefront, vendor-panel added
This commit is contained in:
45
backend/packages/modules/algolia/package.json
Normal file
45
backend/packages/modules/algolia/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/algolia",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
11
backend/packages/modules/algolia/src/index.ts
Normal file
11
backend/packages/modules/algolia/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from "@medusajs/framework/utils";
|
||||
|
||||
import AlgoliaModuleService from "./service";
|
||||
|
||||
export const ALGOLIA_MODULE = "algolia";
|
||||
export { AlgoliaModuleService };
|
||||
export { defaultProductSettings, defaultReviewSettings } from "./service";
|
||||
|
||||
export default Module(ALGOLIA_MODULE, {
|
||||
service: AlgoliaModuleService,
|
||||
});
|
||||
142
backend/packages/modules/algolia/src/service.ts
Normal file
142
backend/packages/modules/algolia/src/service.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
Action,
|
||||
Algoliasearch,
|
||||
BatchRequest,
|
||||
IndexSettings,
|
||||
algoliasearch,
|
||||
} from "algoliasearch";
|
||||
import { IndexType, AlgoliaEntity } from "@mercurjs/framework";
|
||||
|
||||
type ModuleOptions = {
|
||||
appId: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
export const defaultProductSettings: IndexSettings = {
|
||||
searchableAttributes: [
|
||||
"title",
|
||||
"subtitle",
|
||||
"brand.name",
|
||||
"tags.value",
|
||||
"type.value",
|
||||
"categories.name",
|
||||
"collection.title",
|
||||
"variants.title",
|
||||
],
|
||||
};
|
||||
|
||||
export const defaultReviewSettings: IndexSettings = {
|
||||
attributesForFaceting: ["filterOnly(reference_id)", "filterOnly(reference)"],
|
||||
};
|
||||
|
||||
class AlgoliaModuleService {
|
||||
private options_: ModuleOptions;
|
||||
private algolia_: Algoliasearch;
|
||||
|
||||
constructor(_, options: ModuleOptions) {
|
||||
this.options_ = options;
|
||||
this.algolia_ = algoliasearch(this.options_.appId, this.options_.apiKey);
|
||||
}
|
||||
|
||||
getAppId() {
|
||||
return this.options_.appId;
|
||||
}
|
||||
|
||||
checkIndex(index: IndexType) {
|
||||
return this.algolia_.indexExists({
|
||||
indexName: index,
|
||||
});
|
||||
}
|
||||
|
||||
updateSettings(index: IndexType, settings: IndexSettings) {
|
||||
return this.algolia_.setSettings({
|
||||
indexName: index,
|
||||
indexSettings: settings,
|
||||
});
|
||||
}
|
||||
|
||||
batch(type: IndexType, toAdd: AlgoliaEntity[], toDelete: string[]) {
|
||||
const requests: BatchRequest[] = toAdd.map((entity) => {
|
||||
return {
|
||||
action: "addObject" as Action,
|
||||
objectID: entity.id,
|
||||
body: entity,
|
||||
};
|
||||
});
|
||||
|
||||
requests.concat(
|
||||
toDelete.map((id) => {
|
||||
return {
|
||||
action: "deleteObject" as Action,
|
||||
objectID: id,
|
||||
body: {},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return this.algolia_.batch({
|
||||
indexName: type,
|
||||
batchWriteParams: {
|
||||
requests,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
batchUpsert(type: IndexType, entities: AlgoliaEntity[]) {
|
||||
return this.algolia_.batch({
|
||||
indexName: type,
|
||||
batchWriteParams: {
|
||||
requests: entities.map((entity) => {
|
||||
return {
|
||||
action: "addObject",
|
||||
objectID: entity.id,
|
||||
body: entity,
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
batchDelete(type: IndexType, ids: string[]) {
|
||||
return this.algolia_.batch({
|
||||
indexName: type,
|
||||
batchWriteParams: {
|
||||
requests: ids.map((id) => {
|
||||
return {
|
||||
action: "deleteObject",
|
||||
objectID: id,
|
||||
body: {},
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
upsert(type: IndexType, entity: AlgoliaEntity) {
|
||||
return this.algolia_.addOrUpdateObject({
|
||||
indexName: type,
|
||||
objectID: entity.id,
|
||||
body: entity,
|
||||
});
|
||||
}
|
||||
|
||||
delete(type: IndexType, id: string) {
|
||||
return this.algolia_.deleteObject({
|
||||
indexName: type,
|
||||
objectID: id,
|
||||
});
|
||||
}
|
||||
|
||||
partialUpdate(
|
||||
type: IndexType,
|
||||
entity: Partial<AlgoliaEntity> & { id: string }
|
||||
) {
|
||||
return this.algolia_.partialUpdateObject({
|
||||
indexName: type,
|
||||
objectID: entity.id,
|
||||
attributesToUpdate: { ...entity },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AlgoliaModuleService;
|
||||
27
backend/packages/modules/algolia/tsconfig.json
Normal file
27
backend/packages/modules/algolia/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"]
|
||||
}
|
||||
45
backend/packages/modules/attribute/package.json
Normal file
45
backend/packages/modules/attribute/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/attribute",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
10
backend/packages/modules/attribute/src/index.ts
Normal file
10
backend/packages/modules/attribute/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@medusajs/framework/utils";
|
||||
|
||||
import AttributeModuleService from "./service";
|
||||
|
||||
export const ATTRIBUTE_MODULE = "attribute";
|
||||
export { AttributeModuleService };
|
||||
|
||||
export default Module(ATTRIBUTE_MODULE, {
|
||||
service: AttributeModuleService,
|
||||
});
|
||||
@@ -0,0 +1,422 @@
|
||||
{
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"is_filterable": {
|
||||
"name": "is_filterable",
|
||||
"type": "boolean",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "true",
|
||||
"mappedType": "boolean"
|
||||
},
|
||||
"handle": {
|
||||
"name": "handle",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"ui_component": {
|
||||
"name": "ui_component",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "'select'",
|
||||
"enumItems": [
|
||||
"select",
|
||||
"multivalue",
|
||||
"unit",
|
||||
"toggle",
|
||||
"text_area",
|
||||
"color_picker"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "attribute",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_attribute_handle_unique",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_attribute_handle_unique\" ON \"attribute\" (handle) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_attribute_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_attribute_deleted_at\" ON \"attribute\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "attribute_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"rank": {
|
||||
"name": "rank",
|
||||
"type": "integer",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"attribute_id": {
|
||||
"name": "attribute_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "attribute_possible_value",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_attribute_possible_value_attribute_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_attribute_possible_value_attribute_id\" ON \"attribute_possible_value\" (attribute_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_attribute_possible_value_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_attribute_possible_value_deleted_at\" ON \"attribute_possible_value\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "UQ_attribute_id_value",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"UQ_attribute_id_value\" ON \"attribute_possible_value\" (attribute_id, value) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "attribute_possible_value_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"attribute_possible_value_attribute_id_foreign": {
|
||||
"constraintName": "attribute_possible_value_attribute_id_foreign",
|
||||
"columnNames": [
|
||||
"attribute_id"
|
||||
],
|
||||
"localTableName": "public.attribute_possible_value",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.attribute",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"rank": {
|
||||
"name": "rank",
|
||||
"type": "integer",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"attribute_id": {
|
||||
"name": "attribute_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "attribute_value",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_attribute_value_attribute_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_attribute_value_attribute_id\" ON \"attribute_value\" (attribute_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_attribute_value_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_attribute_value_deleted_at\" ON \"attribute_value\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "attribute_value_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"attribute_value_attribute_id_foreign": {
|
||||
"constraintName": "attribute_value_attribute_id_foreign",
|
||||
"columnNames": [
|
||||
"attribute_id"
|
||||
],
|
||||
"localTableName": "public.attribute_value",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.attribute",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
}
|
||||
],
|
||||
"nativeEnums": {}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250617080244 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table if exists "attribute" drop constraint if exists "attribute_handle_unique";`);
|
||||
this.addSql(`create table if not exists "attribute" ("id" text not null, "name" text not null, "description" text null, "handle" text not null, "metadata" jsonb null, "ui_component" text check ("ui_component" in ('select', 'multivalue', 'unit', 'toggle', 'text_area', 'color_picker')) not null default 'select', "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "attribute_pkey" primary key ("id"));`);
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_attribute_handle_unique" ON "attribute" (handle) WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_attribute_deleted_at" ON "attribute" (deleted_at) WHERE deleted_at IS NULL;`);
|
||||
|
||||
this.addSql(`create table if not exists "attribute_possible_value" ("id" text not null, "value" text not null, "rank" integer not null, "metadata" jsonb null, "attribute_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "attribute_possible_value_pkey" primary key ("id"));`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_attribute_possible_value_attribute_id" ON "attribute_possible_value" (attribute_id) WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_attribute_possible_value_deleted_at" ON "attribute_possible_value" (deleted_at) WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "UQ_attribute_id_value" ON "attribute_possible_value" (attribute_id, value) WHERE deleted_at IS NULL;`);
|
||||
|
||||
this.addSql(`create table if not exists "attribute_value" ("id" text not null, "value" text not null, "rank" integer not null, "metadata" jsonb null, "attribute_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "attribute_value_pkey" primary key ("id"));`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_attribute_value_attribute_id" ON "attribute_value" (attribute_id) WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_attribute_value_deleted_at" ON "attribute_value" (deleted_at) WHERE deleted_at IS NULL;`);
|
||||
|
||||
this.addSql(`alter table if exists "attribute_possible_value" add constraint "attribute_possible_value_attribute_id_foreign" foreign key ("attribute_id") references "attribute" ("id") on update cascade on delete cascade;`);
|
||||
|
||||
this.addSql(`alter table if exists "attribute_value" add constraint "attribute_value_attribute_id_foreign" foreign key ("attribute_id") references "attribute" ("id") on update cascade on delete cascade;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table if exists "attribute_possible_value" drop constraint if exists "attribute_possible_value_attribute_id_foreign";`);
|
||||
|
||||
this.addSql(`alter table if exists "attribute_value" drop constraint if exists "attribute_value_attribute_id_foreign";`);
|
||||
|
||||
this.addSql(`drop table if exists "attribute" cascade;`);
|
||||
|
||||
this.addSql(`drop table if exists "attribute_possible_value" cascade;`);
|
||||
|
||||
this.addSql(`drop table if exists "attribute_value" cascade;`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250620110849 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table if exists "attribute" add column if not exists "is_filterable" boolean not null default true;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table if exists "attribute" drop column if exists "is_filterable";`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
import Attribute from './attribute'
|
||||
|
||||
const AttributePossibleValue = model
|
||||
.define('attribute_possible_value', {
|
||||
id: model.id({ prefix: 'attr_pos_val' }).primaryKey(),
|
||||
value: model.text(),
|
||||
rank: model.number(),
|
||||
metadata: model.json().nullable(),
|
||||
attribute: model.belongsTo(() => Attribute, {
|
||||
mappedBy: 'possible_values'
|
||||
})
|
||||
})
|
||||
.indexes([
|
||||
{
|
||||
on: ['attribute_id', 'value'],
|
||||
name: 'UQ_attribute_id_value',
|
||||
unique: true
|
||||
}
|
||||
])
|
||||
|
||||
export default AttributePossibleValue
|
||||
@@ -0,0 +1,15 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
import Attribute from './attribute'
|
||||
|
||||
const AttributeValue = model.define('attribute_value', {
|
||||
id: model.id({ prefix: 'attr_val' }).primaryKey(),
|
||||
value: model.text(),
|
||||
rank: model.number(),
|
||||
metadata: model.json().nullable(),
|
||||
attribute: model.belongsTo(() => Attribute, {
|
||||
mappedBy: 'values'
|
||||
})
|
||||
})
|
||||
|
||||
export default AttributeValue
|
||||
25
backend/packages/modules/attribute/src/models/attribute.ts
Normal file
25
backend/packages/modules/attribute/src/models/attribute.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { model } from "@medusajs/framework/utils";
|
||||
|
||||
import { AttributeUIComponent } from "@mercurjs/framework";
|
||||
import AttributePossibleValue from "./attribute-possible-value";
|
||||
import AttributeValue from "./attribute-value";
|
||||
|
||||
const Attribute = model
|
||||
.define("attribute", {
|
||||
id: model.id({ prefix: "attr" }).primaryKey(),
|
||||
name: model.text().searchable(),
|
||||
description: model.text().nullable(),
|
||||
is_filterable: model.boolean().default(true),
|
||||
handle: model.text().unique(),
|
||||
metadata: model.json().nullable(),
|
||||
ui_component: model
|
||||
.enum(Object.values(AttributeUIComponent))
|
||||
.default(AttributeUIComponent.SELECT),
|
||||
values: model.hasMany(() => AttributeValue),
|
||||
possible_values: model.hasMany(() => AttributePossibleValue),
|
||||
})
|
||||
.cascades({
|
||||
delete: ["values", "possible_values"],
|
||||
});
|
||||
|
||||
export default Attribute;
|
||||
93
backend/packages/modules/attribute/src/service.ts
Normal file
93
backend/packages/modules/attribute/src/service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { EntityManager } from "@mikro-orm/knex";
|
||||
import { UpdateAttributeDTO } from "@mercurjs/framework";
|
||||
import { Context, DAL, InferTypeOf } from "@medusajs/framework/types";
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
MedusaService,
|
||||
} from "@medusajs/framework/utils";
|
||||
|
||||
import Attribute from "./models/attribute";
|
||||
import AttributePossibleValue from "./models/attribute-possible-value";
|
||||
import AttributeValue from "./models/attribute-value";
|
||||
|
||||
type Attribute = InferTypeOf<typeof Attribute>;
|
||||
type AttributePossibleValue = InferTypeOf<typeof AttributePossibleValue>;
|
||||
|
||||
type InjectedDependencies = {
|
||||
attributeRepository: DAL.RepositoryService<Attribute>;
|
||||
attributePossibleValueRepository: DAL.RepositoryService<AttributePossibleValue>;
|
||||
};
|
||||
|
||||
class AttributeModuleService extends MedusaService({
|
||||
Attribute,
|
||||
AttributeValue,
|
||||
AttributePossibleValue,
|
||||
}) {
|
||||
protected attributeRepository_: DAL.RepositoryService<Attribute>;
|
||||
protected attributePossibleValueRepository_: DAL.RepositoryService<AttributePossibleValue>;
|
||||
|
||||
constructor({
|
||||
attributeRepository,
|
||||
attributePossibleValueRepository,
|
||||
}: InjectedDependencies) {
|
||||
super(...arguments);
|
||||
this.attributeRepository_ = attributeRepository;
|
||||
this.attributePossibleValueRepository_ = attributePossibleValueRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param input
|
||||
* @param sharedContext
|
||||
*
|
||||
* Useful to update attribute, allowing to upsert possible_values in the same operation. If "id"
|
||||
* is not provided for "possible_values" entries, it will lookup the DB by attributePossibleValue.value,
|
||||
* to update or create accordingly.
|
||||
*
|
||||
* Assumes caller will eventually refetch entities, for now, to reduce complexity of this
|
||||
* method and concentrate on upserting like ProductOption - ProductOptionValue from Medusa
|
||||
*/
|
||||
@InjectManager()
|
||||
async updateAttributeWithUpsertOrReplacePossibleValues(
|
||||
input: UpdateAttributeDTO | UpdateAttributeDTO[],
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
const normalizedInput = Array.isArray(input) ? input : [input];
|
||||
|
||||
return this.updateAttributeWithUpsertOrReplacePossibleValues_(
|
||||
normalizedInput,
|
||||
sharedContext
|
||||
);
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
protected async updateAttributeWithUpsertOrReplacePossibleValues_(
|
||||
input: UpdateAttributeDTO[],
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
const upsertedValues = await this.attributePossibleValueRepository_.upsert(
|
||||
input.flatMap((element) => element.possible_values),
|
||||
sharedContext
|
||||
);
|
||||
|
||||
const attributesInput = input.map((toUpdate) => {
|
||||
const { ...attribute } = toUpdate;
|
||||
return {
|
||||
...attribute,
|
||||
possible_values: upsertedValues
|
||||
.filter((val) => val.attribute_id === attribute.id)
|
||||
.map((upserted) => ({ id: upserted.id })),
|
||||
};
|
||||
});
|
||||
|
||||
return this.attributeRepository_.upsertWithReplace(
|
||||
attributesInput,
|
||||
{ relations: ["possible_values"] },
|
||||
sharedContext
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AttributeModuleService;
|
||||
27
backend/packages/modules/attribute/tsconfig.json
Normal file
27
backend/packages/modules/attribute/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"]
|
||||
}
|
||||
45
backend/packages/modules/brand/package.json
Normal file
45
backend/packages/modules/brand/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/brand",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
10
backend/packages/modules/brand/src/index.ts
Normal file
10
backend/packages/modules/brand/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@medusajs/framework/utils";
|
||||
|
||||
import BrandModuleService from "./service";
|
||||
|
||||
export const BRAND_MODULE = "brand";
|
||||
export { BrandModuleService };
|
||||
|
||||
export default Module(BRAND_MODULE, {
|
||||
service: BrandModuleService,
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "brand",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_brand_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_brand_deleted_at\" ON \"brand\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "brand_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250218113025 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "brand" ("id" text not null, "name" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "brand_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_brand_deleted_at" ON "brand" (deleted_at) WHERE deleted_at IS NULL;');
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "brand" cascade;');
|
||||
}
|
||||
|
||||
}
|
||||
6
backend/packages/modules/brand/src/models/brand.ts
Normal file
6
backend/packages/modules/brand/src/models/brand.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
export const Brand = model.define('brand', {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text()
|
||||
})
|
||||
9
backend/packages/modules/brand/src/service.ts
Normal file
9
backend/packages/modules/brand/src/service.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { MedusaService } from '@medusajs/framework/utils'
|
||||
|
||||
import { Brand } from './models/brand'
|
||||
|
||||
class BrandModuleService extends MedusaService({
|
||||
Brand
|
||||
}) {}
|
||||
|
||||
export default BrandModuleService
|
||||
27
backend/packages/modules/brand/tsconfig.json
Normal file
27
backend/packages/modules/brand/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"]
|
||||
}
|
||||
45
backend/packages/modules/commission/package.json
Normal file
45
backend/packages/modules/commission/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/commission",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
10
backend/packages/modules/commission/src/index.ts
Normal file
10
backend/packages/modules/commission/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@medusajs/framework/utils";
|
||||
|
||||
import CommissionModuleService from "./service";
|
||||
|
||||
export const COMMISSION_MODULE = "commission";
|
||||
export { CommissionModuleService };
|
||||
|
||||
export default Module(COMMISSION_MODULE, {
|
||||
service: CommissionModuleService,
|
||||
});
|
||||
@@ -0,0 +1,388 @@
|
||||
{
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"item_line_id": {
|
||||
"name": "item_line_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"rule_id": {
|
||||
"name": "rule_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"currency_code": {
|
||||
"name": "currency_code",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "numeric",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "decimal"
|
||||
},
|
||||
"raw_value": {
|
||||
"name": "raw_value",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "commission_line",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_commission_line_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_commission_line_deleted_at\" ON \"commission_line\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "commission_line_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"reference": {
|
||||
"name": "reference",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"reference_id": {
|
||||
"name": "reference_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "true",
|
||||
"mappedType": "boolean"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "commission_rule",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_commission_rule_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_commission_rule_deleted_at\" ON \"commission_rule\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "commission_rule_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"percentage_rate": {
|
||||
"name": "percentage_rate",
|
||||
"type": "integer",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"include_tax": {
|
||||
"name": "include_tax",
|
||||
"type": "boolean",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "boolean"
|
||||
},
|
||||
"price_set_id": {
|
||||
"name": "price_set_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"max_price_set_id": {
|
||||
"name": "max_price_set_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"min_price_set_id": {
|
||||
"name": "min_price_set_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"rule_id": {
|
||||
"name": "rule_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "commission_rate",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"rule_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "commission_rate_rule_id_unique",
|
||||
"primary": false,
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_commission_rate_rule_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_commission_rate_rule_id\" ON \"commission_rate\" (rule_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_commission_rate_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_commission_rate_deleted_at\" ON \"commission_rate\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "commission_rate_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"commission_rate_rule_id_foreign": {
|
||||
"constraintName": "commission_rate_rule_id_foreign",
|
||||
"columnNames": [
|
||||
"rule_id"
|
||||
],
|
||||
"localTableName": "public.commission_rate",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.commission_rule",
|
||||
"deleteRule": "set null",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250124152358 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "commission_line" ("id" text not null, "item_line_id" text not null, "rule_id" text not null, "currency_code" text not null, "value" numeric not null, "raw_value" jsonb not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "commission_line_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_commission_line_deleted_at" ON "commission_line" (deleted_at) WHERE deleted_at IS NULL;');
|
||||
|
||||
this.addSql('create table if not exists "commission_rule" ("id" text not null, "name" text not null, "reference" text not null, "reference_id" text not null, "is_active" boolean not null default true, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "commission_rule_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_commission_rule_deleted_at" ON "commission_rule" (deleted_at) WHERE deleted_at IS NULL;');
|
||||
|
||||
this.addSql('create table if not exists "commission_rate" ("id" text not null, "type" text not null, "percentage_rate" integer not null, "include_tax" boolean not null, "price_set_id" text not null, "max_price_set_id" text not null, "min_price_set_id" text not null, "rule_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "commission_rate_pkey" primary key ("id"));');
|
||||
this.addSql('alter table if exists "commission_rate" add constraint "commission_rate_rule_id_unique" unique ("rule_id");');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_commission_rate_rule_id" ON "commission_rate" (rule_id) WHERE deleted_at IS NULL;');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_commission_rate_deleted_at" ON "commission_rate" (deleted_at) WHERE deleted_at IS NULL;');
|
||||
|
||||
this.addSql('alter table if exists "commission_rate" add constraint "commission_rate_rule_id_foreign" foreign key ("rule_id") references "commission_rule" ("id") on update cascade on delete set null;');
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('alter table if exists "commission_rate" drop constraint if exists "commission_rate_rule_id_foreign";');
|
||||
|
||||
this.addSql('drop table if exists "commission_line" cascade;');
|
||||
|
||||
this.addSql('drop table if exists "commission_rule" cascade;');
|
||||
|
||||
this.addSql('drop table if exists "commission_rate" cascade;');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Migration } from '@mikro-orm/migrations'
|
||||
|
||||
export class Migration20250127073504 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "percentage_rate" type integer using ("percentage_rate"::integer);'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "percentage_rate" drop not null;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "price_set_id" type text using ("price_set_id"::text);'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "price_set_id" drop not null;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "max_price_set_id" type text using ("max_price_set_id"::text);'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "max_price_set_id" drop not null;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "min_price_set_id" type text using ("min_price_set_id"::text);'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "min_price_set_id" drop not null;'
|
||||
)
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "percentage_rate" type integer using ("percentage_rate"::integer);'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "percentage_rate" set not null;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "price_set_id" type text using ("price_set_id"::text);'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "price_set_id" set not null;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "max_price_set_id" type text using ("max_price_set_id"::text);'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "max_price_set_id" set not null;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "min_price_set_id" type text using ("min_price_set_id"::text);'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "commission_rate" alter column "min_price_set_id" set not null;'
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
export const CommissionLine = model.define('commission_line', {
|
||||
id: model.id({ prefix: 'com_line' }).primaryKey(),
|
||||
item_line_id: model.text(),
|
||||
rule_id: model.text(),
|
||||
currency_code: model.text(),
|
||||
value: model.bigNumber()
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
import { CommissionRule } from './commission_rule'
|
||||
|
||||
export const CommissionRate = model.define('commission_rate', {
|
||||
id: model.id({ prefix: 'com_rate' }).primaryKey(),
|
||||
type: model.text(),
|
||||
percentage_rate: model.number().nullable(),
|
||||
include_tax: model.boolean(),
|
||||
price_set_id: model.text().nullable(),
|
||||
max_price_set_id: model.text().nullable(),
|
||||
min_price_set_id: model.text().nullable(),
|
||||
rule: model
|
||||
.belongsTo(() => CommissionRule, {
|
||||
mappedBy: 'rate'
|
||||
})
|
||||
.nullable()
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
import { CommissionRate } from './commission_rate'
|
||||
|
||||
export const CommissionRule = model.define('commission_rule', {
|
||||
id: model.id({ prefix: 'com_rule' }).primaryKey(),
|
||||
name: model.text().searchable(),
|
||||
reference: model.text().searchable(),
|
||||
reference_id: model.text(),
|
||||
is_active: model.boolean().default(true),
|
||||
rate: model.hasOne(() => CommissionRate, {
|
||||
mappedBy: 'rule'
|
||||
})
|
||||
})
|
||||
3
backend/packages/modules/commission/src/models/index.ts
Normal file
3
backend/packages/modules/commission/src/models/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './commission_rate'
|
||||
export * from './commission_rule'
|
||||
export * from './commission_line'
|
||||
58
backend/packages/modules/commission/src/service.ts
Normal file
58
backend/packages/modules/commission/src/service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { MedusaService } from "@medusajs/framework/utils";
|
||||
|
||||
import { CommissionRate, CommissionRule } from "./models";
|
||||
import { CommissionLine } from "./models/commission_line";
|
||||
import {
|
||||
CommissionCalculationContext,
|
||||
CommissionRuleDTO,
|
||||
} from "@mercurjs/framework";
|
||||
|
||||
class CommissionModuleService extends MedusaService({
|
||||
CommissionRate,
|
||||
CommissionRule,
|
||||
CommissionLine,
|
||||
}) {
|
||||
private async selectCommissionRule(reference: string, reference_id: string) {
|
||||
const [rule] = await this.listCommissionRules(
|
||||
{ reference, reference_id, is_active: true, deleted_at: null },
|
||||
{ relations: ["rate"] }
|
||||
);
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for first applicable CommissionRule for given context. The queries are executed in assumed priority order.
|
||||
* @param ctx Calculation context
|
||||
* @returns CommissionRule applicable for given context or null
|
||||
*/
|
||||
async selectCommissionForProductLine(
|
||||
ctx: CommissionCalculationContext
|
||||
): Promise<CommissionRuleDTO | null> {
|
||||
const ruleQueries = [
|
||||
{
|
||||
reference: "seller+product_type",
|
||||
reference_id: `${ctx.seller_id}+${ctx.product_type_id}`,
|
||||
},
|
||||
{
|
||||
reference: "seller+product_category",
|
||||
reference_id: `${ctx.seller_id}+${ctx.product_category_id}`,
|
||||
},
|
||||
{ reference: "seller", reference_id: ctx.seller_id },
|
||||
{ reference: "product_type", reference_id: ctx.product_type_id },
|
||||
{ reference: "product_category", reference_id: ctx.product_category_id },
|
||||
{ reference: "site", reference_id: "" },
|
||||
];
|
||||
|
||||
for (const { reference, reference_id } of ruleQueries) {
|
||||
const rule = await this.selectCommissionRule(reference, reference_id);
|
||||
if (rule) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default CommissionModuleService;
|
||||
27
backend/packages/modules/commission/tsconfig.json
Normal file
27
backend/packages/modules/commission/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"]
|
||||
}
|
||||
45
backend/packages/modules/configuration/package.json
Normal file
45
backend/packages/modules/configuration/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/configuration",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
12
backend/packages/modules/configuration/src/index.ts
Normal file
12
backend/packages/modules/configuration/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from "@medusajs/framework/utils";
|
||||
|
||||
import ConfigurationModuleService, {
|
||||
ConfigurationRuleDefaults,
|
||||
} from "./service";
|
||||
|
||||
export const CONFIGURATION_MODULE = "configuration";
|
||||
export { ConfigurationModuleService, ConfigurationRuleDefaults };
|
||||
|
||||
export default Module(CONFIGURATION_MODULE, {
|
||||
service: ConfigurationModuleService,
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"rule_type": {
|
||||
"name": "rule_type",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"enumItems": [
|
||||
"global_product_catalog",
|
||||
"require_product_approval",
|
||||
"product_request_enabled",
|
||||
"product_import_enabled"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"is_enabled": {
|
||||
"name": "is_enabled",
|
||||
"type": "boolean",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "boolean"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "configuration_rule",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_configuration_rule_rule_type_unique",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_configuration_rule_rule_type_unique\" ON \"configuration_rule\" (rule_type) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_configuration_rule_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_configuration_rule_deleted_at\" ON \"configuration_rule\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "configuration_rule_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
}
|
||||
],
|
||||
"nativeEnums": {}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250114063624 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "configuration_rule" ("id" text not null, "rule_type" text check ("rule_type" in (\'global_product_catalog\', \'require_product_approval\', \'product_request_enabled\')) not null, "is_enabled" boolean not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "configuration_rule_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_configuration_rule_rule_type_unique" ON "configuration_rule" (rule_type) WHERE deleted_at IS NULL;');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_configuration_rule_deleted_at" ON "configuration_rule" (deleted_at) WHERE deleted_at IS NULL;');
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "configuration_rule" cascade;');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250324131111 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table if exists "configuration_rule" drop constraint if exists "configuration_rule_rule_type_check";`);
|
||||
|
||||
this.addSql(`alter table if exists "configuration_rule" add constraint "configuration_rule_rule_type_check" check("rule_type" in ('global_product_catalog', 'require_product_approval', 'product_request_enabled', 'product_import_enabled'));`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table if exists "configuration_rule" drop constraint if exists "configuration_rule_rule_type_check";`);
|
||||
|
||||
this.addSql(`alter table if exists "configuration_rule" add constraint "configuration_rule_rule_type_check" check("rule_type" in ('global_product_catalog', 'require_product_approval', 'product_request_enabled'));`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { model } from "@medusajs/framework/utils";
|
||||
|
||||
export const ConfigurationRule = model.define("configuration_rule", {
|
||||
id: model.id({ prefix: "conf" }).primaryKey(),
|
||||
rule_type: model
|
||||
.enum([
|
||||
"global_product_catalog",
|
||||
"require_product_approval",
|
||||
"product_request_enabled",
|
||||
"product_import_enabled",
|
||||
])
|
||||
.unique(),
|
||||
is_enabled: model.boolean(),
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './configuration-rule'
|
||||
27
backend/packages/modules/configuration/src/service.ts
Normal file
27
backend/packages/modules/configuration/src/service.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { MedusaService } from "@medusajs/framework/utils";
|
||||
|
||||
import { ConfigurationRule } from "./models";
|
||||
import { ConfigurationRuleType } from "@mercurjs/framework";
|
||||
|
||||
export const ConfigurationRuleDefaults = new Map<
|
||||
ConfigurationRuleType,
|
||||
boolean
|
||||
>([
|
||||
[ConfigurationRuleType.GLOBAL_PRODUCT_CATALOG, false],
|
||||
[ConfigurationRuleType.PRODUCT_REQUEST_ENABLED, true],
|
||||
[ConfigurationRuleType.REQUIRE_PRODUCT_APPROVAL, false],
|
||||
[ConfigurationRuleType.PRODUCT_IMPORT_ENABLED, true],
|
||||
]);
|
||||
|
||||
class ConfigurationModuleService extends MedusaService({
|
||||
ConfigurationRule,
|
||||
}) {
|
||||
async isRuleEnabled(type: ConfigurationRuleType): Promise<boolean> {
|
||||
const [rule] = await this.listConfigurationRules({
|
||||
rule_type: type,
|
||||
});
|
||||
return rule ? rule.is_enabled : ConfigurationRuleDefaults.get(type)!;
|
||||
}
|
||||
}
|
||||
|
||||
export default ConfigurationModuleService;
|
||||
27
backend/packages/modules/configuration/tsconfig.json
Normal file
27
backend/packages/modules/configuration/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"]
|
||||
}
|
||||
44
backend/packages/modules/marketplace/package.json
Normal file
44
backend/packages/modules/marketplace/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@mercurjs/marketplace",
|
||||
"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",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
10
backend/packages/modules/marketplace/src/index.ts
Normal file
10
backend/packages/modules/marketplace/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@medusajs/framework/utils";
|
||||
|
||||
import MarketplaceModuleService from "./service";
|
||||
|
||||
export const MARKETPLACE_MODULE = "marketplace";
|
||||
export { MarketplaceModuleService };
|
||||
|
||||
export default Module(MARKETPLACE_MODULE, {
|
||||
service: MarketplaceModuleService,
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Migration } from '@mikro-orm/migrations'
|
||||
|
||||
export class Migration20241207151814 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
this.addSql(
|
||||
'create table if not exists "order_set" ("id" text not null, "display_id" serial, "sales_channel_id" text not null, "cart_id" text not null, "customer_id" text null, "payment_collection_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "order_set_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_order_set_deleted_at" ON "order_set" (deleted_at) WHERE deleted_at IS NULL;'
|
||||
)
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "order_set" cascade;')
|
||||
}
|
||||
}
|
||||
1
backend/packages/modules/marketplace/src/models/index.ts
Normal file
1
backend/packages/modules/marketplace/src/models/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './order-set'
|
||||
10
backend/packages/modules/marketplace/src/models/order-set.ts
Normal file
10
backend/packages/modules/marketplace/src/models/order-set.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
export const OrderSet = model.define('order_set', {
|
||||
id: model.id({ prefix: 'ordset' }).primaryKey(),
|
||||
display_id: model.number().nullable(),
|
||||
sales_channel_id: model.text(),
|
||||
cart_id: model.text(),
|
||||
customer_id: model.text().nullable(),
|
||||
payment_collection_id: model.text()
|
||||
})
|
||||
9
backend/packages/modules/marketplace/src/service.ts
Normal file
9
backend/packages/modules/marketplace/src/service.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { MedusaService } from '@medusajs/framework/utils'
|
||||
|
||||
import { OrderSet } from './models'
|
||||
|
||||
class MarketplaceModuleService extends MedusaService({
|
||||
OrderSet
|
||||
}) {}
|
||||
|
||||
export default MarketplaceModuleService
|
||||
27
backend/packages/modules/marketplace/tsconfig.json
Normal file
27
backend/packages/modules/marketplace/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"]
|
||||
}
|
||||
45
backend/packages/modules/order-return-request/package.json
Normal file
45
backend/packages/modules/order-return-request/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/order-return-request",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
10
backend/packages/modules/order-return-request/src/index.ts
Normal file
10
backend/packages/modules/order-return-request/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@medusajs/framework/utils";
|
||||
|
||||
import OrderReturnModuleService from "./service";
|
||||
|
||||
export const ORDER_RETURN_MODULE = "order_return";
|
||||
export { OrderReturnModuleService };
|
||||
|
||||
export default Module(ORDER_RETURN_MODULE, {
|
||||
service: OrderReturnModuleService,
|
||||
});
|
||||
@@ -0,0 +1,309 @@
|
||||
{
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"customer_note": {
|
||||
"name": "customer_note",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"shipping_option_id": {
|
||||
"name": "shipping_option_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"vendor_reviewer_id": {
|
||||
"name": "vendor_reviewer_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"vendor_reviewer_note": {
|
||||
"name": "vendor_reviewer_note",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"vendor_review_date": {
|
||||
"name": "vendor_review_date",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"admin_reviewer_id": {
|
||||
"name": "admin_reviewer_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"admin_reviewer_note": {
|
||||
"name": "admin_reviewer_note",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"admin_review_date": {
|
||||
"name": "admin_review_date",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "'pending'",
|
||||
"enumItems": [
|
||||
"pending",
|
||||
"refunded",
|
||||
"withdrawn",
|
||||
"escalated",
|
||||
"canceled"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "order_return_request",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_order_return_request_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_order_return_request_deleted_at\" ON \"order_return_request\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "order_return_request_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"line_item_id": {
|
||||
"name": "line_item_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"reason_id": {
|
||||
"name": "reason_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"quantity": {
|
||||
"name": "quantity",
|
||||
"type": "integer",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"return_request_id": {
|
||||
"name": "return_request_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "order_return_request_line_item",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_order_return_request_line_item_return_request_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_order_return_request_line_item_return_request_id\" ON \"order_return_request_line_item\" (return_request_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_order_return_request_line_item_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_order_return_request_line_item_deleted_at\" ON \"order_return_request_line_item\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "order_return_request_line_item_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"order_return_request_line_item_return_request_id_foreign": {
|
||||
"constraintName": "order_return_request_line_item_return_request_id_foreign",
|
||||
"columnNames": [
|
||||
"return_request_id"
|
||||
],
|
||||
"localTableName": "public.order_return_request_line_item",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.order_return_request",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
}
|
||||
],
|
||||
"nativeEnums": {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250109084331 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "order_return_request" ("id" text not null, "customer_id" text not null, "customer_note" text not null, "vendor_reviewer_id" text null, "vendor_reviewer_note" text null, "vendor_review_date" timestamptz null, "admin_reviewer_id" text null, "admin_reviewer_note" text null, "admin_review_date" timestamptz null, "status" text check ("status" in (\'pending\', \'refunded\', \'withdrawn\', \'escalated\', \'canceled\')) not null default \'pending\', "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "order_return_request_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_order_return_request_deleted_at" ON "order_return_request" (deleted_at) WHERE deleted_at IS NULL;');
|
||||
|
||||
this.addSql('create table if not exists "order_return_request_line_item" ("id" text not null, "line_item_id" text not null, "quantity" integer not null, "return_request_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "order_return_request_line_item_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_order_return_request_line_item_return_request_id" ON "order_return_request_line_item" (return_request_id) WHERE deleted_at IS NULL;');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_order_return_request_line_item_deleted_at" ON "order_return_request_line_item" (deleted_at) WHERE deleted_at IS NULL;');
|
||||
|
||||
this.addSql('alter table if exists "order_return_request_line_item" add constraint "order_return_request_line_item_return_request_id_foreign" foreign key ("return_request_id") references "order_return_request" ("id") on update cascade;');
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('alter table if exists "order_return_request_line_item" drop constraint if exists "order_return_request_line_item_return_request_id_foreign";');
|
||||
|
||||
this.addSql('drop table if exists "order_return_request" cascade;');
|
||||
|
||||
this.addSql('drop table if exists "order_return_request_line_item" cascade;');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Migration } from '@mikro-orm/migrations'
|
||||
|
||||
export class Migration20250317132949 extends Migration {
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table if exists "order_return_request" add column if not exists "shipping_option_id" text null;`
|
||||
)
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table if exists "order_return_request" drop column if exists "shipping_option_id";`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Migration } from "@mikro-orm/migrations";
|
||||
|
||||
export class Migration20250716063044 extends Migration {
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table if exists "order_return_request_line_item" add column if not exists "reason_id" text null;`
|
||||
);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table if exists "order_return_request_line_item" drop column if exists "reason_id";`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './return-request'
|
||||
export * from './return-request-line-item'
|
||||
@@ -0,0 +1,16 @@
|
||||
import { model } from "@medusajs/framework/utils";
|
||||
|
||||
import { OrderReturnRequest } from "./return-request";
|
||||
|
||||
export const OrderReturnRequestLineItem = model.define(
|
||||
"order_return_request_line_item",
|
||||
{
|
||||
id: model.id({ prefix: "oretreqli" }).primaryKey(),
|
||||
line_item_id: model.text(),
|
||||
quantity: model.number(),
|
||||
reason_id: model.text().nullable(),
|
||||
return_request: model.belongsTo(() => OrderReturnRequest, {
|
||||
mappedBy: "line_items",
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
import { OrderReturnRequestLineItem } from './return-request-line-item'
|
||||
|
||||
export const OrderReturnRequest = model.define('order_return_request', {
|
||||
id: model.id({ prefix: 'oretreq' }).primaryKey(),
|
||||
customer_id: model.text(),
|
||||
customer_note: model.text(),
|
||||
shipping_option_id: model.text().nullable(),
|
||||
vendor_reviewer_id: model.text().nullable(),
|
||||
vendor_reviewer_note: model.text().nullable(),
|
||||
vendor_review_date: model.dateTime().nullable(),
|
||||
admin_reviewer_id: model.text().nullable(),
|
||||
admin_reviewer_note: model.text().nullable(),
|
||||
admin_review_date: model.dateTime().nullable(),
|
||||
line_items: model.hasMany(() => OrderReturnRequestLineItem, {
|
||||
mappedBy: 'return_request'
|
||||
}),
|
||||
status: model
|
||||
.enum(['pending', 'refunded', 'withdrawn', 'escalated', 'canceled'])
|
||||
.default('pending')
|
||||
})
|
||||
10
backend/packages/modules/order-return-request/src/service.ts
Normal file
10
backend/packages/modules/order-return-request/src/service.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { MedusaService } from '@medusajs/framework/utils'
|
||||
|
||||
import { OrderReturnRequest, OrderReturnRequestLineItem } from './models'
|
||||
|
||||
class OrderReturnModuleService extends MedusaService({
|
||||
OrderReturnRequest,
|
||||
OrderReturnRequestLineItem
|
||||
}) {}
|
||||
|
||||
export default OrderReturnModuleService
|
||||
27
backend/packages/modules/order-return-request/tsconfig.json
Normal file
27
backend/packages/modules/order-return-request/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"]
|
||||
}
|
||||
45
backend/packages/modules/payment-stripe-connect/package.json
Normal file
45
backend/packages/modules/payment-stripe-connect/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/payment-stripe-connect",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
import Stripe from "stripe";
|
||||
|
||||
import {
|
||||
ProviderWebhookPayload,
|
||||
WebhookActionResult,
|
||||
} from "@medusajs/framework/types";
|
||||
import {
|
||||
AbstractPaymentProvider,
|
||||
MedusaError,
|
||||
PaymentActions,
|
||||
PaymentSessionStatus,
|
||||
isPresent,
|
||||
} from "@medusajs/framework/utils";
|
||||
import {
|
||||
AuthorizePaymentInput,
|
||||
AuthorizePaymentOutput,
|
||||
CancelPaymentInput,
|
||||
CancelPaymentOutput,
|
||||
CapturePaymentInput,
|
||||
CapturePaymentOutput,
|
||||
DeletePaymentInput,
|
||||
DeletePaymentOutput,
|
||||
GetPaymentStatusInput,
|
||||
GetPaymentStatusOutput,
|
||||
InitiatePaymentInput,
|
||||
InitiatePaymentOutput,
|
||||
RefundPaymentInput,
|
||||
RefundPaymentOutput,
|
||||
RetrievePaymentInput,
|
||||
RetrievePaymentOutput,
|
||||
UpdatePaymentInput,
|
||||
UpdatePaymentOutput,
|
||||
} from "@medusajs/types";
|
||||
|
||||
import {
|
||||
getAmountFromSmallestUnit,
|
||||
getSmallestUnit,
|
||||
ErrorCodes,
|
||||
ErrorIntentStatus,
|
||||
PaymentIntentOptions,
|
||||
} from "@mercurjs/framework";
|
||||
|
||||
type Options = {
|
||||
apiKey: string;
|
||||
webhookSecret: string;
|
||||
};
|
||||
|
||||
abstract class StripeConnectProvider extends AbstractPaymentProvider<Options> {
|
||||
private readonly options_: Options;
|
||||
private readonly client_: Stripe;
|
||||
|
||||
constructor(container, options: Options) {
|
||||
super(container);
|
||||
|
||||
this.options_ = options;
|
||||
|
||||
this.client_ = new Stripe(options.apiKey);
|
||||
}
|
||||
|
||||
abstract get paymentIntentOptions(): PaymentIntentOptions;
|
||||
|
||||
async getPaymentStatus(
|
||||
input: GetPaymentStatusInput
|
||||
): Promise<GetPaymentStatusOutput> {
|
||||
const id = input.data?.id as string;
|
||||
const paymentIntent = await this.client_.paymentIntents.retrieve(id);
|
||||
const dataResponse = paymentIntent as unknown as Record<string, unknown>;
|
||||
|
||||
switch (paymentIntent.status) {
|
||||
case "requires_payment_method":
|
||||
case "requires_confirmation":
|
||||
case "processing":
|
||||
return { status: PaymentSessionStatus.PENDING, data: dataResponse };
|
||||
case "requires_action":
|
||||
return {
|
||||
status: PaymentSessionStatus.REQUIRES_MORE,
|
||||
data: dataResponse,
|
||||
};
|
||||
case "canceled":
|
||||
return { status: PaymentSessionStatus.CANCELED, data: dataResponse };
|
||||
case "requires_capture":
|
||||
return { status: PaymentSessionStatus.AUTHORIZED, data: dataResponse };
|
||||
case "succeeded":
|
||||
return { status: PaymentSessionStatus.CAPTURED, data: dataResponse };
|
||||
default:
|
||||
return { status: PaymentSessionStatus.PENDING, data: dataResponse };
|
||||
}
|
||||
}
|
||||
|
||||
async initiatePayment(
|
||||
input: InitiatePaymentInput
|
||||
): Promise<InitiatePaymentOutput> {
|
||||
const { amount, currency_code } = input;
|
||||
|
||||
const email = input.context?.customer?.email;
|
||||
|
||||
const paymentIntentInput: Stripe.PaymentIntentCreateParams = {
|
||||
...this.paymentIntentOptions,
|
||||
currency: currency_code,
|
||||
amount: getSmallestUnit(amount, currency_code),
|
||||
};
|
||||
|
||||
// revisit when you could update customer using initiatePayment
|
||||
try {
|
||||
const {
|
||||
data: [customer],
|
||||
} = await this.client_.customers.list({
|
||||
email,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (customer) {
|
||||
paymentIntentInput.customer = customer.id;
|
||||
}
|
||||
} catch (error) {
|
||||
throw this.buildError(
|
||||
"An error occurred in initiatePayment when retrieving a Stripe customer",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (!paymentIntentInput.customer) {
|
||||
try {
|
||||
const customer = await this.client_.customers.create({ email });
|
||||
paymentIntentInput.customer = customer.id;
|
||||
} catch (error) {
|
||||
throw this.buildError(
|
||||
"An error occurred in initiatePayment when creating a Stripe customer",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data = (await this.client_.paymentIntents.create(
|
||||
paymentIntentInput
|
||||
)) as any;
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
data,
|
||||
};
|
||||
} catch (error) {
|
||||
throw this.buildError(
|
||||
"An error occurred in initiatePayment when creating a Stripe payment intent",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async authorizePayment(
|
||||
data: AuthorizePaymentInput
|
||||
): Promise<AuthorizePaymentOutput> {
|
||||
const result = await this.getPaymentStatus(data);
|
||||
if (result.status === PaymentSessionStatus.CAPTURED) {
|
||||
return { status: PaymentSessionStatus.AUTHORIZED, data: result.data };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async cancelPayment({
|
||||
data: paymentSessionData,
|
||||
}: CancelPaymentInput): Promise<CancelPaymentOutput> {
|
||||
try {
|
||||
const id = paymentSessionData?.id as string;
|
||||
|
||||
if (!id) {
|
||||
return { data: paymentSessionData };
|
||||
}
|
||||
|
||||
const data = (await this.client_.paymentIntents.cancel(id)) as any;
|
||||
return { data };
|
||||
} catch (error) {
|
||||
throw this.buildError("An error occurred in cancelPayment", error);
|
||||
}
|
||||
}
|
||||
|
||||
async capturePayment({
|
||||
data: paymentSessionData,
|
||||
}: CapturePaymentInput): Promise<CapturePaymentOutput> {
|
||||
const id = paymentSessionData?.id as string;
|
||||
try {
|
||||
const data = (await this.client_.paymentIntents.capture(id)) as any;
|
||||
return { data };
|
||||
} catch (error) {
|
||||
if (error.code === ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE) {
|
||||
if (error.payment_intent?.status === ErrorIntentStatus.SUCCEEDED) {
|
||||
return { data: error.payment_intent };
|
||||
}
|
||||
}
|
||||
throw this.buildError("An error occurred in capturePayment", error);
|
||||
}
|
||||
}
|
||||
|
||||
deletePayment(data: DeletePaymentInput): Promise<DeletePaymentOutput> {
|
||||
return this.cancelPayment(data);
|
||||
}
|
||||
|
||||
async refundPayment({
|
||||
data: paymentSessionData,
|
||||
amount,
|
||||
}: RefundPaymentInput): Promise<RefundPaymentOutput> {
|
||||
const id = paymentSessionData?.id as string;
|
||||
|
||||
try {
|
||||
const currency = paymentSessionData?.currency as string;
|
||||
await this.client_.refunds.create({
|
||||
amount: getSmallestUnit(amount, currency),
|
||||
payment_intent: id as string,
|
||||
});
|
||||
} catch (e) {
|
||||
throw this.buildError("An error occurred in refundPayment", e);
|
||||
}
|
||||
|
||||
return { data: paymentSessionData };
|
||||
}
|
||||
|
||||
async retrievePayment({
|
||||
data: paymentSessionData,
|
||||
}: RetrievePaymentInput): Promise<RetrievePaymentOutput> {
|
||||
try {
|
||||
const id = paymentSessionData?.id as string;
|
||||
const intent = (await this.client_.paymentIntents.retrieve(id)) as any;
|
||||
|
||||
intent.amount = getAmountFromSmallestUnit(intent.amount, intent.currency);
|
||||
console.log("Stripe - retrieving", intent);
|
||||
return { data: intent };
|
||||
} catch (e) {
|
||||
throw this.buildError("An error occurred in retrievePayment", e);
|
||||
}
|
||||
}
|
||||
|
||||
async updatePayment(input: UpdatePaymentInput): Promise<UpdatePaymentOutput> {
|
||||
const { data, amount, currency_code } = input;
|
||||
|
||||
const amountNumeric = getSmallestUnit(amount, currency_code);
|
||||
|
||||
if (isPresent(amount) && data?.amount === amountNumeric) {
|
||||
return { data };
|
||||
}
|
||||
|
||||
try {
|
||||
const id = data?.id as string;
|
||||
const sessionData = (await this.client_.paymentIntents.update(id, {
|
||||
amount: amountNumeric,
|
||||
})) as any;
|
||||
|
||||
return { data: sessionData };
|
||||
} catch (e) {
|
||||
throw this.buildError("An error occurred in updatePayment", e);
|
||||
}
|
||||
}
|
||||
|
||||
async updatePaymentData(sessionId: string, data: Record<string, unknown>) {
|
||||
try {
|
||||
// Prevent from updating the amount from here as it should go through
|
||||
// the updatePayment method to perform the correct logic
|
||||
if (isPresent(data.amount)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Cannot update amount, use updatePayment instead"
|
||||
);
|
||||
}
|
||||
|
||||
return (await this.client_.paymentIntents.update(sessionId, {
|
||||
...data,
|
||||
})) as any;
|
||||
} catch (e) {
|
||||
throw this.buildError("An error occurred in updatePaymentData", e);
|
||||
}
|
||||
}
|
||||
|
||||
async getWebhookActionAndData(
|
||||
webhookData: ProviderWebhookPayload["payload"]
|
||||
): Promise<WebhookActionResult> {
|
||||
const event = this.constructWebhookEvent(webhookData);
|
||||
const intent = event.data.object as Stripe.PaymentIntent;
|
||||
|
||||
const { currency } = intent;
|
||||
switch (event.type) {
|
||||
case "payment_intent.amount_capturable_updated":
|
||||
return {
|
||||
action: PaymentActions.AUTHORIZED,
|
||||
data: {
|
||||
session_id: intent.metadata.session_id,
|
||||
amount: getAmountFromSmallestUnit(
|
||||
intent.amount_capturable,
|
||||
currency
|
||||
),
|
||||
},
|
||||
};
|
||||
case "payment_intent.succeeded":
|
||||
return {
|
||||
action: PaymentActions.SUCCESSFUL,
|
||||
data: {
|
||||
session_id: intent.metadata.session_id,
|
||||
amount: getAmountFromSmallestUnit(intent.amount_received, currency),
|
||||
},
|
||||
};
|
||||
case "payment_intent.payment_failed":
|
||||
return {
|
||||
action: PaymentActions.FAILED,
|
||||
data: {
|
||||
session_id: intent.metadata.session_id,
|
||||
amount: getAmountFromSmallestUnit(intent.amount, currency),
|
||||
},
|
||||
};
|
||||
default:
|
||||
return { action: PaymentActions.NOT_SUPPORTED };
|
||||
}
|
||||
}
|
||||
|
||||
constructWebhookEvent(data: ProviderWebhookPayload["payload"]): Stripe.Event {
|
||||
const signature = data.headers["stripe-signature"] as string;
|
||||
|
||||
return this.client_.webhooks.constructEvent(
|
||||
data.rawData as string | Buffer,
|
||||
signature,
|
||||
this.options_.webhookSecret
|
||||
);
|
||||
}
|
||||
|
||||
private buildError(message: string, error: Error) {
|
||||
return new MedusaError(
|
||||
MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR,
|
||||
`${message}: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StripeConnectProvider;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ModuleProvider, Modules } from '@medusajs/framework/utils'
|
||||
|
||||
import StripeConnectCardProviderService from './services/stripe-connect-card-provider'
|
||||
|
||||
export default ModuleProvider(Modules.PAYMENT, {
|
||||
services: [StripeConnectCardProviderService]
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import StripeConnectProvider from "../core/stripe-connect-provider";
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "@mercurjs/framework";
|
||||
|
||||
class StripeConnectCardProviderService extends StripeConnectProvider {
|
||||
static identifier = PaymentProviderKeys.CARD;
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options);
|
||||
}
|
||||
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["card"],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default StripeConnectCardProviderService;
|
||||
@@ -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"]
|
||||
}
|
||||
45
backend/packages/modules/payout/package.json
Normal file
45
backend/packages/modules/payout/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/payout",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
10
backend/packages/modules/payout/src/index.ts
Normal file
10
backend/packages/modules/payout/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@medusajs/framework/utils";
|
||||
|
||||
import PayoutModuleService from "./service";
|
||||
|
||||
export const PAYOUT_MODULE = "payout";
|
||||
export { PayoutModuleService };
|
||||
|
||||
export default Module(PAYOUT_MODULE, {
|
||||
service: PayoutModuleService,
|
||||
});
|
||||
@@ -0,0 +1,518 @@
|
||||
{
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "'pending'",
|
||||
"enumItems": [
|
||||
"pending",
|
||||
"active",
|
||||
"disabled"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"reference_id": {
|
||||
"name": "reference_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"context": {
|
||||
"name": "context",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "payout_account",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_payout_account_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payout_account_deleted_at\" ON \"payout_account\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "payout_account_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"currency_code": {
|
||||
"name": "currency_code",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "numeric",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "decimal"
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"payout_account_id": {
|
||||
"name": "payout_account_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"raw_amount": {
|
||||
"name": "raw_amount",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "payout",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_payout_payout_account_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payout_payout_account_id\" ON \"payout\" (payout_account_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_payout_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payout_deleted_at\" ON \"payout\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "payout_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"payout_payout_account_id_foreign": {
|
||||
"constraintName": "payout_payout_account_id_foreign",
|
||||
"columnNames": [
|
||||
"payout_account_id"
|
||||
],
|
||||
"localTableName": "public.payout",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.payout_account",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"context": {
|
||||
"name": "context",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"payout_account_id": {
|
||||
"name": "payout_account_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "onboarding",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_onboarding_payout_account_id_unique",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_onboarding_payout_account_id_unique\" ON \"onboarding\" (payout_account_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_onboarding_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_onboarding_deleted_at\" ON \"onboarding\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "onboarding_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"onboarding_payout_account_id_foreign": {
|
||||
"constraintName": "onboarding_payout_account_id_foreign",
|
||||
"columnNames": [
|
||||
"payout_account_id"
|
||||
],
|
||||
"localTableName": "public.onboarding",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.payout_account",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"currency_code": {
|
||||
"name": "currency_code",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "numeric",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "decimal"
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"payout_id": {
|
||||
"name": "payout_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"raw_amount": {
|
||||
"name": "raw_amount",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "payout_reversal",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_payout_reversal_payout_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payout_reversal_payout_id\" ON \"payout_reversal\" (payout_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_payout_reversal_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_payout_reversal_deleted_at\" ON \"payout_reversal\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "payout_reversal_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"payout_reversal_payout_id_foreign": {
|
||||
"constraintName": "payout_reversal_payout_id_foreign",
|
||||
"columnNames": [
|
||||
"payout_id"
|
||||
],
|
||||
"localTableName": "public.payout_reversal",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.payout",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
}
|
||||
],
|
||||
"nativeEnums": {}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250317090626 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table if exists "onboarding" drop constraint if exists "onboarding_payout_account_id_unique";`);
|
||||
this.addSql(`create table if not exists "payout_account" ("id" text not null, "status" text check ("status" in ('pending', 'active', 'disabled')) not null default 'pending', "reference_id" text not null, "data" jsonb not null, "context" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "payout_account_pkey" primary key ("id"));`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_payout_account_deleted_at" ON "payout_account" (deleted_at) WHERE deleted_at IS NULL;`);
|
||||
|
||||
this.addSql(`create table if not exists "payout" ("id" text not null, "currency_code" text not null, "amount" numeric not null, "data" jsonb null, "payout_account_id" text not null, "raw_amount" jsonb not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "payout_pkey" primary key ("id"));`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_payout_payout_account_id" ON "payout" (payout_account_id) WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_payout_deleted_at" ON "payout" (deleted_at) WHERE deleted_at IS NULL;`);
|
||||
|
||||
this.addSql(`create table if not exists "onboarding" ("id" text not null, "data" jsonb null, "context" jsonb null, "payout_account_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "onboarding_pkey" primary key ("id"));`);
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_onboarding_payout_account_id_unique" ON "onboarding" (payout_account_id) WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_onboarding_deleted_at" ON "onboarding" (deleted_at) WHERE deleted_at IS NULL;`);
|
||||
|
||||
this.addSql(`alter table if exists "payout" add constraint "payout_payout_account_id_foreign" foreign key ("payout_account_id") references "payout_account" ("id") on update cascade;`);
|
||||
|
||||
this.addSql(`alter table if exists "onboarding" add constraint "onboarding_payout_account_id_foreign" foreign key ("payout_account_id") references "payout_account" ("id") on update cascade;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table if exists "payout" drop constraint if exists "payout_payout_account_id_foreign";`);
|
||||
|
||||
this.addSql(`alter table if exists "onboarding" drop constraint if exists "onboarding_payout_account_id_foreign";`);
|
||||
|
||||
this.addSql(`drop table if exists "payout_account" cascade;`);
|
||||
|
||||
this.addSql(`drop table if exists "payout" cascade;`);
|
||||
|
||||
this.addSql(`drop table if exists "onboarding" cascade;`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Migration } from '@mikro-orm/migrations'
|
||||
|
||||
export class Migration20250612144913 extends Migration {
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(
|
||||
`create table if not exists "payout_reversal" ("id" text not null, "currency_code" text not null, "amount" numeric not null, "data" jsonb null, "payout_id" text not null, "raw_amount" jsonb not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "payout_reversal_pkey" primary key ("id"));`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_payout_reversal_payout_id" ON "payout_reversal" (payout_id) WHERE deleted_at IS NULL;`
|
||||
)
|
||||
this.addSql(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_payout_reversal_deleted_at" ON "payout_reversal" (deleted_at) WHERE deleted_at IS NULL;`
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
`alter table if exists "payout_reversal" add constraint "payout_reversal_payout_id_foreign" foreign key ("payout_id") references "payout" ("id") on update cascade;`
|
||||
)
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table if exists "payout_reversal" drop constraint if exists "payout_reversal_payout_id_foreign";`
|
||||
)
|
||||
|
||||
this.addSql(`drop table if exists "payout_reversal" cascade;`)
|
||||
}
|
||||
}
|
||||
4
backend/packages/modules/payout/src/models/index.ts
Normal file
4
backend/packages/modules/payout/src/models/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './payout-account'
|
||||
export * from './payout'
|
||||
export * from './onboarding'
|
||||
export * from './payout-reversal'
|
||||
12
backend/packages/modules/payout/src/models/onboarding.ts
Normal file
12
backend/packages/modules/payout/src/models/onboarding.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
import { PayoutAccount } from './payout-account'
|
||||
|
||||
export const Onboarding = model.define('onboarding', {
|
||||
id: model.id({ prefix: 'onb' }).primaryKey(),
|
||||
data: model.json().nullable(),
|
||||
context: model.json().nullable(),
|
||||
payout_account: model.belongsTo(() => PayoutAccount, {
|
||||
mappedBy: 'onboarding'
|
||||
})
|
||||
})
|
||||
15
backend/packages/modules/payout/src/models/payout-account.ts
Normal file
15
backend/packages/modules/payout/src/models/payout-account.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { model } from "@medusajs/framework/utils";
|
||||
|
||||
import { PayoutAccountStatus } from "@mercurjs/framework";
|
||||
import { Onboarding } from "./onboarding";
|
||||
import { Payout } from "./payout";
|
||||
|
||||
export const PayoutAccount = model.define("payout_account", {
|
||||
id: model.id({ prefix: "pacc" }).primaryKey(),
|
||||
status: model.enum(PayoutAccountStatus).default(PayoutAccountStatus.PENDING),
|
||||
reference_id: model.text(),
|
||||
data: model.json(),
|
||||
context: model.json().nullable(),
|
||||
onboarding: model.hasOne(() => Onboarding).nullable(),
|
||||
payouts: model.hasMany(() => Payout),
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
import { Payout } from './payout'
|
||||
|
||||
export const PayoutReversal = model.define('payout_reversal', {
|
||||
id: model.id({ prefix: 'prev' }).primaryKey(),
|
||||
currency_code: model.text(),
|
||||
amount: model.bigNumber(),
|
||||
data: model.json().nullable(),
|
||||
payout: model.belongsTo(() => Payout, {
|
||||
mappedBy: 'reversals'
|
||||
})
|
||||
})
|
||||
15
backend/packages/modules/payout/src/models/payout.ts
Normal file
15
backend/packages/modules/payout/src/models/payout.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
import { PayoutAccount } from './payout-account'
|
||||
import { PayoutReversal } from './payout-reversal'
|
||||
|
||||
export const Payout = model.define('payout', {
|
||||
id: model.id({ prefix: 'pout' }).primaryKey(),
|
||||
currency_code: model.text(),
|
||||
amount: model.bigNumber(),
|
||||
data: model.json().nullable(),
|
||||
payout_account: model.belongsTo(() => PayoutAccount, {
|
||||
mappedBy: 'payouts'
|
||||
}),
|
||||
reversals: model.hasMany(() => PayoutReversal)
|
||||
})
|
||||
229
backend/packages/modules/payout/src/service.ts
Normal file
229
backend/packages/modules/payout/src/service.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { EntityManager } from "@mikro-orm/knex";
|
||||
|
||||
import { Context } from "@medusajs/framework/types";
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
MedusaService,
|
||||
} from "@medusajs/framework/utils";
|
||||
|
||||
import { Onboarding, Payout, PayoutAccount, PayoutReversal } from "./models";
|
||||
import {
|
||||
CreateOnboardingDTO,
|
||||
CreatePayoutAccountDTO,
|
||||
CreatePayoutDTO,
|
||||
CreatePayoutReversalDTO,
|
||||
IPayoutProvider,
|
||||
PayoutAccountStatus,
|
||||
PayoutWebhookActionPayload,
|
||||
} from "@mercurjs/framework";
|
||||
|
||||
type InjectedDependencies = {
|
||||
payoutProvider: IPayoutProvider;
|
||||
};
|
||||
|
||||
class PayoutModuleService extends MedusaService({
|
||||
Payout,
|
||||
PayoutReversal,
|
||||
PayoutAccount,
|
||||
Onboarding,
|
||||
}) {
|
||||
protected provider_: IPayoutProvider;
|
||||
|
||||
constructor({ payoutProvider }: InjectedDependencies) {
|
||||
super(...arguments);
|
||||
this.provider_ = payoutProvider;
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async createPayoutAccount(
|
||||
{ context }: CreatePayoutAccountDTO,
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
const result = await this.createPayoutAccounts(
|
||||
{ context, reference_id: "placeholder", data: {} },
|
||||
sharedContext
|
||||
);
|
||||
|
||||
try {
|
||||
const { data, id: referenceId } =
|
||||
await this.provider_.createPayoutAccount({
|
||||
context,
|
||||
account_id: result.id,
|
||||
});
|
||||
|
||||
await this.updatePayoutAccounts(
|
||||
{
|
||||
id: result.id,
|
||||
data,
|
||||
reference_id: referenceId,
|
||||
},
|
||||
sharedContext
|
||||
);
|
||||
|
||||
const updated = await this.retrievePayoutAccount(
|
||||
result.id,
|
||||
undefined,
|
||||
sharedContext
|
||||
);
|
||||
return updated;
|
||||
} catch (error) {
|
||||
await this.deletePayoutAccounts(result.id, sharedContext);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async syncStripeAccount(
|
||||
account_id: string,
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
const payout_account = await this.retrievePayoutAccount(account_id);
|
||||
const stripe_account = await this.provider_.getAccount(
|
||||
payout_account.reference_id
|
||||
);
|
||||
|
||||
const status =
|
||||
stripe_account.details_submitted &&
|
||||
stripe_account.payouts_enabled &&
|
||||
stripe_account.charges_enabled &&
|
||||
stripe_account.tos_acceptance &&
|
||||
stripe_account.tos_acceptance?.date !== null;
|
||||
|
||||
await this.updatePayoutAccounts(
|
||||
{
|
||||
id: account_id,
|
||||
data: stripe_account as unknown as Record<string, unknown>,
|
||||
status: status
|
||||
? PayoutAccountStatus.ACTIVE
|
||||
: PayoutAccountStatus.PENDING,
|
||||
},
|
||||
sharedContext
|
||||
);
|
||||
|
||||
const updated = await this.retrievePayoutAccount(
|
||||
account_id,
|
||||
undefined,
|
||||
sharedContext
|
||||
);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async initializeOnboarding(
|
||||
{ context, payout_account_id }: CreateOnboardingDTO,
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
const [existingOnboarding] = await this.listOnboardings({
|
||||
payout_account_id,
|
||||
});
|
||||
const account = await this.retrievePayoutAccount(payout_account_id);
|
||||
|
||||
const { data: providerData } = await this.provider_.initializeOnboarding(
|
||||
account.reference_id!,
|
||||
context
|
||||
);
|
||||
|
||||
let onboarding = existingOnboarding;
|
||||
if (!existingOnboarding) {
|
||||
onboarding = await super.createOnboardings(
|
||||
{
|
||||
payout_account_id,
|
||||
},
|
||||
sharedContext
|
||||
);
|
||||
}
|
||||
|
||||
await this.updateOnboardings(
|
||||
{
|
||||
id: onboarding.id,
|
||||
data: providerData,
|
||||
context,
|
||||
},
|
||||
sharedContext
|
||||
);
|
||||
|
||||
return await this.retrieveOnboarding(
|
||||
onboarding.id,
|
||||
undefined,
|
||||
sharedContext
|
||||
);
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async createPayout(
|
||||
input: CreatePayoutDTO,
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
const {
|
||||
amount,
|
||||
currency_code,
|
||||
account_id,
|
||||
transaction_id,
|
||||
source_transaction,
|
||||
} = input;
|
||||
|
||||
const payoutAccount = await this.retrievePayoutAccount(account_id);
|
||||
|
||||
const { data } = await this.provider_.createPayout({
|
||||
account_reference_id: payoutAccount.reference_id,
|
||||
amount,
|
||||
currency: currency_code,
|
||||
transaction_id,
|
||||
source_transaction,
|
||||
});
|
||||
|
||||
// @ts-expect-error BigNumber incompatible interface
|
||||
const payout = await this.createPayouts(
|
||||
{
|
||||
data,
|
||||
amount,
|
||||
currency_code,
|
||||
payout_account: payoutAccount.id,
|
||||
},
|
||||
sharedContext
|
||||
);
|
||||
|
||||
return payout;
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async createPayoutReversal(
|
||||
input: CreatePayoutReversalDTO,
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
const payout = await this.retrievePayout(input.payout_id);
|
||||
|
||||
if (!payout || !payout.data || !payout.data.id) {
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Payout not found");
|
||||
}
|
||||
|
||||
const transfer_id = payout.data.id as string;
|
||||
|
||||
const transferReversal = await this.provider_.reversePayout({
|
||||
transfer_id,
|
||||
amount: input.amount,
|
||||
currency: input.currency_code,
|
||||
});
|
||||
|
||||
// @ts-expect-error BigNumber incompatible interface
|
||||
const payoutReversal = await this.createPayoutReversals(
|
||||
{
|
||||
data: transferReversal as unknown as Record<string, unknown>,
|
||||
amount: input.amount,
|
||||
currency_code: input.currency_code,
|
||||
payout: payout.id,
|
||||
},
|
||||
sharedContext
|
||||
);
|
||||
|
||||
return payoutReversal;
|
||||
}
|
||||
|
||||
async getWebhookActionAndData(input: PayoutWebhookActionPayload) {
|
||||
return await this.provider_.getWebhookActionAndData(input);
|
||||
}
|
||||
}
|
||||
|
||||
export default PayoutModuleService;
|
||||
1
backend/packages/modules/payout/src/services/index.ts
Normal file
1
backend/packages/modules/payout/src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './provider'
|
||||
214
backend/packages/modules/payout/src/services/provider.ts
Normal file
214
backend/packages/modules/payout/src/services/provider.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import Stripe from "stripe";
|
||||
|
||||
import { ConfigModule, Logger } from "@medusajs/framework/types";
|
||||
import { MedusaError, isPresent } from "@medusajs/framework/utils";
|
||||
|
||||
import { PAYOUT_MODULE } from "..";
|
||||
|
||||
import {
|
||||
CreatePayoutAccountInput,
|
||||
CreatePayoutAccountResponse,
|
||||
IPayoutProvider,
|
||||
InitializeOnboardingResponse,
|
||||
PayoutWebhookAction,
|
||||
PayoutWebhookActionPayload,
|
||||
ProcessPayoutInput,
|
||||
ProcessPayoutResponse,
|
||||
ReversePayoutInput,
|
||||
getSmallestUnit,
|
||||
} from "@mercurjs/framework";
|
||||
|
||||
type InjectedDependencies = {
|
||||
logger: Logger;
|
||||
configModule: ConfigModule;
|
||||
};
|
||||
|
||||
type StripeConnectConfig = {
|
||||
apiKey: string;
|
||||
webhookSecret: string;
|
||||
};
|
||||
|
||||
export class PayoutProvider implements IPayoutProvider {
|
||||
protected readonly config_: StripeConnectConfig;
|
||||
protected readonly logger_: Logger;
|
||||
protected readonly client_: Stripe;
|
||||
|
||||
constructor({ logger, configModule }: InjectedDependencies) {
|
||||
this.logger_ = logger;
|
||||
|
||||
const moduleDef = configModule.modules?.[PAYOUT_MODULE];
|
||||
if (typeof moduleDef !== "boolean" && moduleDef?.options) {
|
||||
this.config_ = {
|
||||
apiKey: moduleDef.options.apiKey as string,
|
||||
webhookSecret: moduleDef.options.webhookSecret as string,
|
||||
};
|
||||
}
|
||||
|
||||
this.client_ = new Stripe(this.config_.apiKey, {
|
||||
apiVersion: "2025-02-24.acacia",
|
||||
});
|
||||
}
|
||||
|
||||
async createPayout({
|
||||
amount,
|
||||
currency,
|
||||
account_reference_id,
|
||||
transaction_id,
|
||||
source_transaction,
|
||||
}: ProcessPayoutInput): Promise<ProcessPayoutResponse> {
|
||||
try {
|
||||
this.logger_.info(
|
||||
`Processing payout for transaction with ID ${transaction_id}`
|
||||
);
|
||||
|
||||
const transfer = await this.client_.transfers.create(
|
||||
{
|
||||
currency,
|
||||
destination: account_reference_id,
|
||||
amount: getSmallestUnit(amount, currency),
|
||||
source_transaction,
|
||||
metadata: {
|
||||
transaction_id,
|
||||
},
|
||||
},
|
||||
{ idempotencyKey: transaction_id }
|
||||
);
|
||||
|
||||
return {
|
||||
data: transfer as unknown as Record<string, unknown>,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger_.error("Error occured while creating payout", error);
|
||||
|
||||
const message = error?.message ?? "Error occured while creating payout";
|
||||
|
||||
throw new MedusaError(MedusaError.Types.UNEXPECTED_STATE, message);
|
||||
}
|
||||
}
|
||||
|
||||
async createPayoutAccount({
|
||||
context,
|
||||
account_id,
|
||||
}: CreatePayoutAccountInput): Promise<CreatePayoutAccountResponse> {
|
||||
try {
|
||||
const { country } = context;
|
||||
this.logger_.info("Creating payment profile");
|
||||
|
||||
if (!isPresent(country)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`"country" is required`
|
||||
);
|
||||
}
|
||||
|
||||
const account = await this.client_.accounts.create({
|
||||
country: country as string,
|
||||
type: "standard",
|
||||
metadata: {
|
||||
account_id,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: account as unknown as Record<string, unknown>,
|
||||
id: account.id,
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
error?.message ?? "Error occured while creating payout account";
|
||||
throw new MedusaError(MedusaError.Types.UNEXPECTED_STATE, message);
|
||||
}
|
||||
}
|
||||
|
||||
async initializeOnboarding(
|
||||
accountId: string,
|
||||
context: Record<string, unknown>
|
||||
): Promise<InitializeOnboardingResponse> {
|
||||
try {
|
||||
this.logger_.info("Initializing onboarding");
|
||||
|
||||
if (!isPresent(context.refresh_url)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`'refresh_url' is required`
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPresent(context.return_url)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`'return_url' is required`
|
||||
);
|
||||
}
|
||||
|
||||
const accountLink = await this.client_.accountLinks.create({
|
||||
account: accountId,
|
||||
refresh_url: context.refresh_url as string,
|
||||
return_url: context.return_url as string,
|
||||
type: "account_onboarding",
|
||||
});
|
||||
|
||||
return {
|
||||
data: accountLink as unknown as Record<string, unknown>,
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
error?.message ?? "Error occured while initializing onboarding";
|
||||
throw new MedusaError(MedusaError.Types.UNEXPECTED_STATE, message);
|
||||
}
|
||||
}
|
||||
|
||||
async getAccount(accountId: string): Promise<Stripe.Account> {
|
||||
try {
|
||||
const account = await this.client_.accounts.retrieve(accountId);
|
||||
return account;
|
||||
} catch (error) {
|
||||
const message = error?.message ?? "Error occured while getting account";
|
||||
throw new MedusaError(MedusaError.Types.UNEXPECTED_STATE, message);
|
||||
}
|
||||
}
|
||||
|
||||
async reversePayout(input: ReversePayoutInput) {
|
||||
try {
|
||||
const reversal = await this.client_.transfers.createReversal(
|
||||
input.transfer_id,
|
||||
{
|
||||
amount: getSmallestUnit(input.amount, input.currency),
|
||||
}
|
||||
);
|
||||
|
||||
return reversal;
|
||||
} catch (error) {
|
||||
const message = error?.message ?? "Error occured while reversing payout";
|
||||
throw new MedusaError(MedusaError.Types.UNEXPECTED_STATE, message);
|
||||
}
|
||||
}
|
||||
|
||||
async getWebhookActionAndData(payload: PayoutWebhookActionPayload) {
|
||||
const signature = payload.headers["stripe-signature"] as string;
|
||||
|
||||
const event = this.client_.webhooks.constructEvent(
|
||||
payload.rawData as string | Buffer,
|
||||
signature,
|
||||
this.config_.webhookSecret
|
||||
);
|
||||
|
||||
const data = event.data.object as Stripe.Account;
|
||||
|
||||
switch (event.type) {
|
||||
case "account.updated":
|
||||
// here you can validate account data to make sure it's valid
|
||||
return {
|
||||
action: PayoutWebhookAction.ACCOUNT_AUTHORIZED,
|
||||
data: {
|
||||
account_id: data.metadata?.account_id as string,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
`Unsupported event type: ${event.type}`
|
||||
);
|
||||
}
|
||||
}
|
||||
27
backend/packages/modules/payout/tsconfig.json
Normal file
27
backend/packages/modules/payout/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"]
|
||||
}
|
||||
45
backend/packages/modules/requests/package.json
Normal file
45
backend/packages/modules/requests/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/requests",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
11
backend/packages/modules/requests/src/index.ts
Normal file
11
backend/packages/modules/requests/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from "@medusajs/framework/utils";
|
||||
|
||||
import RequestsModuleService from "./service";
|
||||
|
||||
export const REQUESTS_MODULE = "requests";
|
||||
export { RequestsModuleService };
|
||||
export * from "./utils";
|
||||
|
||||
export default Module(REQUESTS_MODULE, {
|
||||
service: RequestsModuleService,
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"submitter_id": {
|
||||
"name": "submitter_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"reviewer_id": {
|
||||
"name": "reviewer_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"reviewer_note": {
|
||||
"name": "reviewer_note",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "'pending'",
|
||||
"enumItems": [
|
||||
"draft",
|
||||
"pending",
|
||||
"accepted",
|
||||
"rejected"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "request",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_request_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_request_deleted_at\" ON \"request\" (deleted_at) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "request_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
}
|
||||
],
|
||||
"nativeEnums": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250102142456 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "request" ("id" text not null, "type" text not null, "data" jsonb not null, "submitter_id" text not null, "reviewer_id" text null, "reviewer_note" text null, "status" text check ("status" in (\'pending\', \'accepted\', \'rejected\')) not null default \'pending\', "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "request_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_request_deleted_at" ON "request" (deleted_at) WHERE deleted_at IS NULL;');
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "request" cascade;');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250428150914 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table if exists "request" drop constraint if exists "request_status_check";`);
|
||||
|
||||
this.addSql(`alter table if exists "request" add constraint "request_status_check" check("status" in ('draft', 'pending', 'accepted', 'rejected'));`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table if exists "request" drop constraint if exists "request_status_check";`);
|
||||
|
||||
this.addSql(`alter table if exists "request" add constraint "request_status_check" check("status" in ('pending', 'accepted', 'rejected'));`);
|
||||
}
|
||||
|
||||
}
|
||||
1
backend/packages/modules/requests/src/models/index.ts
Normal file
1
backend/packages/modules/requests/src/models/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './request'
|
||||
13
backend/packages/modules/requests/src/models/request.ts
Normal file
13
backend/packages/modules/requests/src/models/request.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { model } from '@medusajs/framework/utils'
|
||||
|
||||
export const Request = model.define('request', {
|
||||
id: model.id({ prefix: 'req' }).primaryKey(),
|
||||
type: model.text(),
|
||||
data: model.json(),
|
||||
submitter_id: model.text(),
|
||||
reviewer_id: model.text().nullable(),
|
||||
reviewer_note: model.text().nullable(),
|
||||
status: model
|
||||
.enum(['draft', 'pending', 'accepted', 'rejected'])
|
||||
.default('pending')
|
||||
})
|
||||
9
backend/packages/modules/requests/src/service.ts
Normal file
9
backend/packages/modules/requests/src/service.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { MedusaService } from "@medusajs/framework/utils";
|
||||
|
||||
import { Request } from "./models";
|
||||
|
||||
class RequestsModuleService extends MedusaService({
|
||||
Request,
|
||||
}) {}
|
||||
|
||||
export default RequestsModuleService;
|
||||
1
backend/packages/modules/requests/src/utils/index.ts
Normal file
1
backend/packages/modules/requests/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./notifications";
|
||||
70
backend/packages/modules/requests/src/utils/notifications.ts
Normal file
70
backend/packages/modules/requests/src/utils/notifications.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { MedusaContainer } from '@medusajs/framework'
|
||||
import { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils'
|
||||
|
||||
interface RequestNotificationParams {
|
||||
container: MedusaContainer
|
||||
requestId: string
|
||||
requestType: string
|
||||
template: string
|
||||
}
|
||||
|
||||
const notificationResources = {
|
||||
product_type: 'value',
|
||||
product_category: 'name',
|
||||
product_collection: 'title',
|
||||
product_tag: 'value',
|
||||
product: 'title'
|
||||
}
|
||||
|
||||
export async function sendVendorUIRequestNotification({
|
||||
container,
|
||||
requestId,
|
||||
requestType,
|
||||
template
|
||||
}: RequestNotificationParams) {
|
||||
const notificationService = container.resolve(Modules.NOTIFICATION)
|
||||
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const {
|
||||
data: [request]
|
||||
} = await query.graph({
|
||||
entity: 'request',
|
||||
fields: ['*'],
|
||||
filters: {
|
||||
id: requestId
|
||||
}
|
||||
})
|
||||
|
||||
if (!request || request.type !== requestType) {
|
||||
return
|
||||
}
|
||||
|
||||
const resource = notificationResources[requestType]
|
||||
const resourceValue = request.data[resource]
|
||||
|
||||
const {
|
||||
data: [member]
|
||||
} = await query.graph({
|
||||
entity: 'member',
|
||||
fields: ['*'],
|
||||
filters: {
|
||||
id: request.submitter_id
|
||||
}
|
||||
})
|
||||
|
||||
if (!member || !member.seller_id) {
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {}
|
||||
payload[resource] = resourceValue
|
||||
|
||||
await notificationService.createNotifications([
|
||||
{
|
||||
to: member.seller_id,
|
||||
channel: 'seller_feed',
|
||||
template,
|
||||
data: { ...payload }
|
||||
}
|
||||
])
|
||||
}
|
||||
27
backend/packages/modules/requests/tsconfig.json
Normal file
27
backend/packages/modules/requests/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"]
|
||||
}
|
||||
45
backend/packages/modules/resend/package.json
Normal file
45
backend/packages/modules/resend/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mercurjs/resend",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
interface EmailTemplateProps {
|
||||
data: {
|
||||
request_type: string
|
||||
seller_name: string
|
||||
}
|
||||
}
|
||||
|
||||
export const AdminRequestCreatedEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: 600,
|
||||
margin: '0 auto',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
color: '#222',
|
||||
background: '#fff',
|
||||
padding: 24,
|
||||
borderRadius: 10
|
||||
}}>
|
||||
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
|
||||
Hello, <span role="img" aria-label="wave">👋</span>
|
||||
</h1>
|
||||
<p style={{ fontSize: '1.1rem', marginBottom: 16 }}>
|
||||
{data.seller_name} has requested to create a new {data.request_type}. Please review the request and approve it in admin panel.
|
||||
</p>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<div>Best regards,</div>
|
||||
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
|
||||
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
interface EmailTemplateProps {
|
||||
data: {
|
||||
request_address: string,
|
||||
seller_name: string
|
||||
}
|
||||
}
|
||||
|
||||
export const AdminSellerRequestCreatedEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: 600,
|
||||
margin: '0 auto',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
color: '#222',
|
||||
background: '#fff',
|
||||
padding: 24,
|
||||
borderRadius: 10
|
||||
}}>
|
||||
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
|
||||
Hello, <span role="img" aria-label="wave">👋</span>
|
||||
</h1>
|
||||
<p style={{ fontSize: '1.1rem', marginBottom: 16 }}>
|
||||
{data.seller_name} has requested to join the platform. Please review the request and approve it in admin panel.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<a href={data.request_address}>
|
||||
<button>Review Request</button>
|
||||
</a>
|
||||
</div>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<div>Best regards,</div>
|
||||
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
|
||||
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
interface EmailTemplateProps {
|
||||
data: {
|
||||
user_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const BuyerAccountCreatedEmailTemplate: React.FC<
|
||||
Readonly<EmailTemplateProps>
|
||||
> = ({ data }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "480px",
|
||||
margin: "40px auto",
|
||||
padding: "32px 24px",
|
||||
borderRadius: "12px",
|
||||
background: "#fff",
|
||||
boxShadow: "0 2px 12px rgba(0,0,0,0.07)",
|
||||
fontFamily: "Arial, sans-serif",
|
||||
color: "#222",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: "2rem", marginBottom: "16px", color: "#222" }}>
|
||||
Welcome to Mercur, {data.user_name}!
|
||||
</h1>
|
||||
<p style={{ fontSize: "1.1rem", marginBottom: "24px" }}>
|
||||
We’re excited to have you join us on this journey.
|
||||
<br />
|
||||
Your account has been created successfully.
|
||||
</p>
|
||||
<a
|
||||
href="https://mercurjs.com"
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "12px 28px",
|
||||
background: "#222",
|
||||
color: "#fff",
|
||||
borderRadius: "6px",
|
||||
textDecoration: "none",
|
||||
fontWeight: 600,
|
||||
marginBottom: "32px",
|
||||
}}
|
||||
>
|
||||
Visit Mercur
|
||||
</a>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<div>Best regards,</div>
|
||||
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
|
||||
<div style={{ color: "#888", marginTop: 4 }}>mercurjs.com</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
interface EmailTemplateProps {
|
||||
data: {
|
||||
order_address: string,
|
||||
order: {
|
||||
id: string,
|
||||
display_id?: string,
|
||||
items: any[]
|
||||
item?: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const BuyerCancelOrderEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
|
||||
const { order } = data;
|
||||
const itemsArray = order.items?.flat() || [order.item].flat();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: 600,
|
||||
margin: '0 auto',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
color: '#222',
|
||||
background: '#fff',
|
||||
padding: 24,
|
||||
borderRadius: 10
|
||||
}}>
|
||||
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>Your order has been canceled</h1>
|
||||
<p style={{ fontSize: '1.1rem', marginBottom: 24 }}>
|
||||
We're sorry, but your order <b>#{order.display_id || order.id}</b> has been canceled by the seller.
|
||||
</p>
|
||||
|
||||
<h3 style={{ marginTop: 32, marginBottom: 12 }}>Order items:</h3>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: 32 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', padding: '8px', borderBottom: '1px solid #eee' }}>Product</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Amount</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Qty</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{itemsArray.map((item: any, idx: number) => (
|
||||
<tr key={item.id || idx} style={{ borderBottom: '1px solid #f3f3f3' }}>
|
||||
<td style={{ padding: '12px 8px', verticalAlign: 'top' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{item.thumbnail && (
|
||||
<img
|
||||
src={item.thumbnail}
|
||||
alt={item.product_title}
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
objectFit: 'cover',
|
||||
borderRadius: 6,
|
||||
marginRight: 12,
|
||||
border: '1px solid #eee'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>{item.product_title}</div>
|
||||
<div style={{ fontSize: 13, color: '#555' }}>
|
||||
Variant: {item.variant_title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
|
||||
{item.unit_price} eur
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
|
||||
{item.quantity}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
|
||||
{item.unit_price * item.quantity} eur
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style={{ margin: '32px 0 16px 0', fontSize: '1rem' }}>
|
||||
If you have any questions, please contact <b>Mercur Support</b>.
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<a
|
||||
href={data.order_address}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '10px 24px',
|
||||
background: '#222',
|
||||
color: '#fff',
|
||||
borderRadius: 6,
|
||||
textDecoration: 'none',
|
||||
fontWeight: 600,
|
||||
marginBottom: 8
|
||||
}}
|
||||
>
|
||||
View Order Details
|
||||
</a>
|
||||
<div style={{ fontSize: 13, color: '#555', marginTop: 8 }}>
|
||||
If you can’t click the button, here’s your link: <br />
|
||||
<span style={{ color: '#0070f3' }}>{data.order_address}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 13, color: '#888', marginBottom: 24 }}>
|
||||
You received this email because you made a purchase or sale on the Mercur marketplace.
|
||||
If you have any questions, please contact our support team.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<div>Best regards,</div>
|
||||
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
|
||||
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
interface EmailTemplateProps {
|
||||
data: {
|
||||
user_name: string,
|
||||
order_address: string,
|
||||
order_id: string,
|
||||
order: {
|
||||
display_id: string,
|
||||
items: any[],
|
||||
currency_code: string,
|
||||
shipping_methods: {
|
||||
amount: number,
|
||||
name: string
|
||||
}[],
|
||||
total: number,
|
||||
email: string,
|
||||
shipping_address: {
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
company: string,
|
||||
address_1: string,
|
||||
address_2: string,
|
||||
city: string,
|
||||
province: string,
|
||||
postal_code: string,
|
||||
phone: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const BuyerNewOrderEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
|
||||
const { order } = data;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: 600,
|
||||
margin: '0 auto',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
color: '#222',
|
||||
background: '#fff',
|
||||
padding: 24,
|
||||
borderRadius: 10
|
||||
}}>
|
||||
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
|
||||
Thank you for your order, {data.user_name}!<br />
|
||||
Your order #{order.display_id} has been placed!
|
||||
</h1>
|
||||
<p style={{ fontSize: '1.1rem', marginBottom: 24 }}>
|
||||
Thank you for placing order #{order.display_id}.<br />
|
||||
</p>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<a
|
||||
href={data.order_address}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '10px 24px',
|
||||
background: '#222',
|
||||
color: '#fff',
|
||||
borderRadius: 6,
|
||||
textDecoration: 'none',
|
||||
fontWeight: 600,
|
||||
marginBottom: 8
|
||||
}}
|
||||
>
|
||||
Order details
|
||||
</a>
|
||||
<div style={{ fontSize: 13, color: '#555', marginTop: 8 }}>
|
||||
If you can’t click the button, here’s your link: <br />
|
||||
<span style={{ color: '#0070f3' }}>{data.order_address}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3 style={{ marginTop: 32, marginBottom: 12 }}>Here’s the breakdown:</h3>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: 32 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', padding: '8px', borderBottom: '1px solid #eee' }}>Product</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Amount</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Qty</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{order.items.map((item: any, index: number) => (
|
||||
<tr key={index} style={{ borderBottom: '1px solid #f3f3f3' }}>
|
||||
<td style={{ padding: '12px 8px', verticalAlign: 'top' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<img
|
||||
src={item.thumbnail}
|
||||
alt={`Thumbnail of ${item.product_title}`}
|
||||
style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
objectFit: 'cover',
|
||||
marginRight: '10px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #eee'
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>{item.product_title}</div>
|
||||
<div style={{ fontSize: '12px', color: '#555' }}>
|
||||
Variant: {item.variant_title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
|
||||
{item.unit_price} {order.currency_code}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>{item.quantity}</td>
|
||||
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
|
||||
{item.unit_price * item.quantity} {order.currency_code}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><b>Delivery:</b></td>
|
||||
<td colSpan={3}>
|
||||
{order.shipping_methods[0].amount} {order.currency_code}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Total:</b></td>
|
||||
<td colSpan={3}>
|
||||
{order.total} {order.currency_code}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div>
|
||||
<p style={{ marginBottom: 4 }}>
|
||||
<strong>Shipping address:</strong><br />
|
||||
{order.shipping_address.first_name} {order.shipping_address.last_name},<br />
|
||||
{order.shipping_address?.company ? `${order.shipping_address.company}, ` : ''}
|
||||
{order.shipping_address.address_1}
|
||||
{order.shipping_address.address_2 && `, ${order.shipping_address.address_2}`}, {order.shipping_address.postal_code} {order.shipping_address.city}
|
||||
{order.shipping_address.province ? `, ${order.shipping_address.province}` : ''}
|
||||
<br />
|
||||
{order.email}, {order.shipping_address.phone}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<strong>Delivery method:</strong><br />
|
||||
{order.shipping_methods[0].name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#888', marginBottom: 24 }}>
|
||||
You received this email because you made a purchase or sale on the Mercur marketplace.<br />
|
||||
If you have any questions, please contact our support team.
|
||||
</div>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<div>Best regards,</div>
|
||||
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
|
||||
<div style={{ color: '#888', marginTop: 4 }}>mercurjs.com</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
interface EmailTemplateProps {
|
||||
data: {
|
||||
user_name: string,
|
||||
host: string,
|
||||
order_id: string,
|
||||
order: {
|
||||
id: string,
|
||||
display_id: string,
|
||||
trackingNumber: string,
|
||||
items: any[],
|
||||
currency_code: string,
|
||||
item_total: number,
|
||||
shipping_methods: {
|
||||
amount: number,
|
||||
name: string
|
||||
}[],
|
||||
total: number
|
||||
email: string
|
||||
shipping_address: {
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
company: string,
|
||||
address_1: string,
|
||||
address_2: string,
|
||||
city: string,
|
||||
province: string,
|
||||
postal_code: string,
|
||||
phone: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const BuyerOrderShippedEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Your order #{data.order.trackingNumber} has been shipped!</h1>
|
||||
<p>The package is on its way and will be in your hands soon.</p>
|
||||
<div>
|
||||
<p>
|
||||
<strong>Shipping address:</strong>
|
||||
</p>
|
||||
<p>
|
||||
{data.order.shipping_address.first_name} {data.order.shipping_address.last_name}
|
||||
,<br />
|
||||
{data.order.shipping_address?.company ? `${data.order.shipping_address.company}, ` : ''}
|
||||
{data.order.shipping_address.address_1}
|
||||
{data.order.shipping_address.address_2}, {data.order.shipping_address.postal_code}{' '}
|
||||
{data.order.shipping_address.city}
|
||||
{data.order.shipping_address.province ? `, ${data.order.shipping_address.province}` : ''}
|
||||
<br />
|
||||
{data.order.email}, {data.order.shipping_address.phone}
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
<a href={`${data.host}/orders/${data.order.id}`}>View Order Details</a>
|
||||
If you can’t click the button, no worries! Here’s your link: {`${data.host}/orders/${data.order.id}`}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You received this email because you made a purchase or sale on the Mercur marketplace. If you have any
|
||||
questions, please contact our support team.
|
||||
</p>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<div>Best regards,</div>
|
||||
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
|
||||
<div style={{ color: '#888', marginTop: 4 }}>mercurjs.com</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
interface EmailTemplateProps {
|
||||
data: {
|
||||
url: string,
|
||||
}
|
||||
}
|
||||
|
||||
export const ForgotPasswordEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: 600,
|
||||
margin: '0 auto',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
color: '#222',
|
||||
background: '#fff',
|
||||
padding: 24,
|
||||
borderRadius: 10
|
||||
}}>
|
||||
<h1>Have you forgotten your password?</h1>
|
||||
<p>
|
||||
We have received a request to reset the password for your Mercur account. Please click the button below to set a
|
||||
new password. Please note, the link is valid for the next 24 hours only.
|
||||
</p>
|
||||
<div>
|
||||
<a href={`${data.url}`}>
|
||||
<button>Reset Password</button>
|
||||
</a>
|
||||
</div>
|
||||
<p>If you did not request this change, please ignore this email.</p>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<div>Best regards,</div>
|
||||
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
|
||||
<div style={{ color: '#888', marginTop: 4 }}>mercurjs.com</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
42
backend/packages/modules/resend/src/email-templates/index.ts
Normal file
42
backend/packages/modules/resend/src/email-templates/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { AdminRequestCreatedEmailTemplate } from "./admin-request-created";
|
||||
import { AdminSellerRequestCreatedEmailTemplate } from "./admin-seller-request-created";
|
||||
import { BuyerAccountCreatedEmailTemplate } from "./buyer-account-created";
|
||||
import { BuyerCancelOrderEmailTemplate } from "./buyer-cancel-order";
|
||||
import { BuyerNewOrderEmailTemplate } from "./buyer-new-order";
|
||||
import { BuyerOrderShippedEmailTemplate } from "./buyer-shipped-order";
|
||||
import { ForgotPasswordEmailTemplate } from "./forgot-password";
|
||||
import { NewSellerInviteEmailTemplate } from "./new-seller-invitation";
|
||||
import { SellerAccountApprovedEmailTemplate } from "./seller-account-approved";
|
||||
import { SellerAccountRejectedEmailTemplate } from "./seller-account-rejected";
|
||||
import { SellerAccountSubmissionEmailTemplate } from "./seller-account-updates-submission";
|
||||
import { SellerCanceledOrderEmailTemplate } from "./seller-canceled-order";
|
||||
import { SellerNewOrderEmailTemplate } from "./seller-new-order";
|
||||
import { SellerPayoutSummaryEmailTemplate } from "./seller-payout-summary";
|
||||
import { SellerProductApprovedEmailTemplate } from "./seller-product-approved";
|
||||
import { SellerProductRejectedEmailTemplate } from "./seller-product-rejected";
|
||||
import { SellerOrderShippingEmailTemplate } from "./seller-shipping-order";
|
||||
import { SellerTeamInviteEmailTemplate } from "./seller-team-invite";
|
||||
import { SellerEmailVerifyEmailTemplate } from "./seller-verify-email";
|
||||
|
||||
export const emailTemplates: any = {
|
||||
buyerAccountCreatedEmailTemplate: BuyerAccountCreatedEmailTemplate,
|
||||
buyerCancelOrderEmailTemplate: BuyerCancelOrderEmailTemplate,
|
||||
buyerNewOrderEmailTemplate: BuyerNewOrderEmailTemplate,
|
||||
buyerOrderShippedEmailTemplate: BuyerOrderShippedEmailTemplate,
|
||||
forgotPasswordEmailTemplate: ForgotPasswordEmailTemplate,
|
||||
sellerAccountApprovedEmailTemplate: SellerAccountApprovedEmailTemplate,
|
||||
sellerAccountRejectedEmailTemplate: SellerAccountRejectedEmailTemplate,
|
||||
sellerAccountSubmissionEmailTemplate: SellerAccountSubmissionEmailTemplate,
|
||||
sellerCanceledOrderEmailTemplate: SellerCanceledOrderEmailTemplate,
|
||||
sellerNewOrderEmailTemplate: SellerNewOrderEmailTemplate,
|
||||
sellerOrderShippingEmailTemplate: SellerOrderShippingEmailTemplate,
|
||||
sellerTeamInviteEmailTemplate: SellerTeamInviteEmailTemplate,
|
||||
sellerVerifyEmailTemplate: SellerEmailVerifyEmailTemplate,
|
||||
newSellerInvitation: NewSellerInviteEmailTemplate,
|
||||
sellerProductRejectedEmailTemplate: SellerProductRejectedEmailTemplate,
|
||||
sellerProductApprovedEmailTemplate: SellerProductApprovedEmailTemplate,
|
||||
adminRequestCreatedEmailTemplate: AdminRequestCreatedEmailTemplate,
|
||||
adminSellerRequestCreatedEmailTemplate:
|
||||
AdminSellerRequestCreatedEmailTemplate,
|
||||
sellerPayoutSummaryEmailTemplate: SellerPayoutSummaryEmailTemplate,
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
interface EmailTemplateProps {
|
||||
data: {
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
export const NewSellerInviteEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: 600,
|
||||
margin: '0 auto',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
color: '#222',
|
||||
background: '#fff',
|
||||
padding: 24,
|
||||
borderRadius: 10
|
||||
}}>
|
||||
<h1 style={{ fontSize: '2rem', marginBottom: '8px' }}>
|
||||
You are invited to sell on MercurJS!
|
||||
</h1>
|
||||
<p style={{ fontSize: '1.1rem', marginBottom: '16px' }}>
|
||||
To join the platform, please accept the invitation.<br />
|
||||
</p>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<a
|
||||
href={`${data.url}`}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '10px 24px',
|
||||
background: '#222',
|
||||
color: '#fff',
|
||||
borderRadius: 6,
|
||||
textDecoration: 'none',
|
||||
fontWeight: 600,
|
||||
marginBottom: 8
|
||||
}}
|
||||
>
|
||||
Accept Invitation
|
||||
</a>
|
||||
<div style={{ fontSize: 13, color: '#555', marginTop: 8 }}>
|
||||
If you can’t click the button, here’s your link: <br />
|
||||
<span style={{ color: '#0070f3' }}>{`${data.url}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#888', marginBottom: 24 }}>
|
||||
You received this email because you were invited to join the Mercur marketplace.<br />
|
||||
If you have any questions, please contact our support team.
|
||||
</div>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<div>Best regards,</div>
|
||||
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
|
||||
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
interface EmailTemplateProps {
|
||||
data: {
|
||||
user_name: string
|
||||
}
|
||||
}
|
||||
|
||||
export const SellerAccountApprovedEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: 600,
|
||||
margin: '0 auto',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
color: '#222',
|
||||
background: '#fff',
|
||||
padding: 24,
|
||||
borderRadius: 10
|
||||
}}>
|
||||
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
|
||||
Hello, {data.user_name} <span role="img" aria-label="wave">👋</span>
|
||||
<br />
|
||||
<span style={{ fontSize: '1.5rem', fontWeight: 600 }}>Your account has been approved!</span>
|
||||
</h1>
|
||||
<p style={{ fontSize: '1.1rem', marginBottom: 16 }}>
|
||||
We’re happy to let you know that your application has been approved! This means your account is now activated on
|
||||
the Mercur marketplace.
|
||||
</p>
|
||||
<p style={{ fontSize: '1.1rem', marginBottom: 24 }}>
|
||||
Thank you for choosing us. We are excited about your success and will support you every step of the way.
|
||||
</p>
|
||||
<div style={{ fontSize: 13, color: '#888', marginBottom: 24 }}>
|
||||
You received this email because you registered as a seller on the Mercur marketplace.<br />
|
||||
If you have any questions, please contact our support team.
|
||||
</div>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<div>Best regards,</div>
|
||||
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
|
||||
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user