Initial commit: backend, storefront, vendor-panel added
This commit is contained in:
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"]
|
||||
}
|
||||
Reference in New Issue
Block a user