Initial commit: backend, storefront, vendor-panel added

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

View File

@@ -0,0 +1,45 @@
{
"name": "@mercurjs/seller",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"!dist/**/__tests__",
"!dist/**/__mocks__",
"!dist/**/__fixtures__"
],
"engines": {
"node": ">=20"
},
"license": "MIT",
"scripts": {
"build": "rimraf dist && tsc --build",
"migration:initial": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial",
"migration:create": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create",
"migration:up": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up",
"orm:cache:clear": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear"
},
"devDependencies": {
"@medusajs/framework": "2.8.6",
"@medusajs/test-utils": "2.8.6",
"@mercurjs/framework": "*",
"@mikro-orm/cli": "6.4.3",
"@mikro-orm/core": "6.4.3",
"@mikro-orm/migrations": "6.4.3",
"@mikro-orm/postgresql": "6.4.3",
"@swc/core": "^1.7.28",
"@swc/jest": "^0.2.36",
"jest": "^29.7.0",
"rimraf": "^3.0.2",
"tsc-alias": "^1.8.6",
"typescript": "^5.6.2"
},
"peerDependencies": {
"@medusajs/framework": "2.8.6",
"@mikro-orm/core": "6.4.3",
"@mikro-orm/migrations": "6.4.3",
"@mikro-orm/postgresql": "6.4.3",
"awilix": "^8.0.1"
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from "@medusajs/framework/utils";
import SellerModuleService from "./service";
export const SELLER_MODULE = "seller";
export { SellerModuleService };
export * from "./utils";
export default Module(SELLER_MODULE, { service: SellerModuleService });

View File

@@ -0,0 +1,671 @@
{
"namespaces": [
"public"
],
"name": "public",
"tables": [
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"store_status": {
"name": "store_status",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "'ACTIVE'",
"enumItems": [
"ACTIVE",
"INACTIVE",
"SUSPENDED"
],
"mappedType": "enum"
},
"name": {
"name": "name",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"handle": {
"name": "handle",
"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"
},
"photo": {
"name": "photo",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"email": {
"name": "email",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"phone": {
"name": "phone",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"address_line": {
"name": "address_line",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"city": {
"name": "city",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"state": {
"name": "state",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"postal_code": {
"name": "postal_code",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"country_code": {
"name": "country_code",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"tax_id": {
"name": "tax_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": "seller",
"schema": "public",
"indexes": [
{
"keyName": "IDX_seller_handle_unique",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_seller_handle_unique\" ON \"seller\" (handle) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_seller_deleted_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_seller_deleted_at\" ON \"seller\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "seller_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"
},
"email": {
"name": "email",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"role": {
"name": "role",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "'owner'",
"enumItems": [
"owner",
"admin",
"member"
],
"mappedType": "enum"
},
"seller_id": {
"name": "seller_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"token": {
"name": "token",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"expires_at": {
"name": "expires_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"mappedType": "datetime"
},
"accepted": {
"name": "accepted",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "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": "member_invite",
"schema": "public",
"indexes": [
{
"keyName": "IDX_member_invite_seller_id",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_member_invite_seller_id\" ON \"member_invite\" (seller_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_member_invite_deleted_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_member_invite_deleted_at\" ON \"member_invite\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "member_invite_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"member_invite_seller_id_foreign": {
"constraintName": "member_invite_seller_id_foreign",
"columnNames": [
"seller_id"
],
"localTableName": "public.member_invite",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.seller",
"updateRule": "cascade"
}
},
"nativeEnums": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"role": {
"name": "role",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "'owner'",
"enumItems": [
"owner",
"admin",
"member"
],
"mappedType": "enum"
},
"name": {
"name": "name",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"email": {
"name": "email",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"bio": {
"name": "bio",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"phone": {
"name": "phone",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"photo": {
"name": "photo",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"seller_id": {
"name": "seller_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": "member",
"schema": "public",
"indexes": [
{
"keyName": "IDX_member_seller_id",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_member_seller_id\" ON \"member\" (seller_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_member_deleted_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_member_deleted_at\" ON \"member\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "member_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"member_seller_id_foreign": {
"constraintName": "member_seller_id_foreign",
"columnNames": [
"seller_id"
],
"localTableName": "public.member",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.seller",
"updateRule": "cascade"
}
},
"nativeEnums": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"store_information": {
"name": "store_information",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "false",
"mappedType": "boolean"
},
"stripe_connection": {
"name": "stripe_connection",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "false",
"mappedType": "boolean"
},
"locations_shipping": {
"name": "locations_shipping",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "false",
"mappedType": "boolean"
},
"products": {
"name": "products",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "false",
"mappedType": "boolean"
},
"seller_id": {
"name": "seller_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": "seller_onboarding",
"schema": "public",
"indexes": [
{
"keyName": "IDX_seller_onboarding_seller_id_unique",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_seller_onboarding_seller_id_unique\" ON \"seller_onboarding\" (seller_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_seller_onboarding_deleted_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_seller_onboarding_deleted_at\" ON \"seller_onboarding\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "seller_onboarding_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"seller_onboarding_seller_id_foreign": {
"constraintName": "seller_onboarding_seller_id_foreign",
"columnNames": [
"seller_id"
],
"localTableName": "public.seller_onboarding",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.seller",
"updateRule": "cascade"
}
},
"nativeEnums": {}
}
],
"nativeEnums": {}
}

View File

@@ -0,0 +1,35 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20241206125415 extends Migration {
async up(): Promise<void> {
this.addSql('create table if not exists "seller" ("id" text not null, "name" text not null, "handle" text not null, "description" text null, "photo" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "seller_pkey" primary key ("id"));');
this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_seller_handle_unique" ON "seller" (handle) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_seller_deleted_at" ON "seller" (deleted_at) WHERE deleted_at IS NULL;');
this.addSql('create table if not exists "member_invite" ("id" text not null, "email" text not null, "role" text check ("role" in (\'owner\', \'admin\', \'member\')) not null default \'owner\', "seller_id" text not null, "token" text not null, "expires_at" timestamptz not null, "accepted" boolean not null default false, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "member_invite_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_member_invite_seller_id" ON "member_invite" (seller_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_member_invite_deleted_at" ON "member_invite" (deleted_at) WHERE deleted_at IS NULL;');
this.addSql('create table if not exists "member" ("id" text not null, "role" text check ("role" in (\'owner\', \'admin\', \'member\')) not null default \'owner\', "name" text not null, "bio" text null, "phone" text null, "photo" text null, "seller_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "member_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_member_seller_id" ON "member" (seller_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_member_deleted_at" ON "member" (deleted_at) WHERE deleted_at IS NULL;');
this.addSql('alter table if exists "member_invite" add constraint "member_invite_seller_id_foreign" foreign key ("seller_id") references "seller" ("id") on update cascade on delete cascade;');
this.addSql('alter table if exists "member" add constraint "member_seller_id_foreign" foreign key ("seller_id") references "seller" ("id") on update cascade on delete cascade;');
}
async down(): Promise<void> {
this.addSql('alter table if exists "member_invite" drop constraint if exists "member_invite_seller_id_foreign";');
this.addSql('alter table if exists "member" drop constraint if exists "member_seller_id_foreign";');
this.addSql('drop table if exists "seller" cascade;');
this.addSql('drop table if exists "member_invite" cascade;');
this.addSql('drop table if exists "member" cascade;');
}
}

View File

@@ -0,0 +1,32 @@
import { Migration } from '@mikro-orm/migrations'
export class Migration20250212131627 extends Migration {
async up(): Promise<void> {
this.addSql('drop table if exists "onboarding" cascade;')
this.addSql(
'alter table if exists "member" add column if not exists "email" text null;'
)
}
async down(): Promise<void> {
this.addSql(
'create table if not exists "onboarding" ("id" text not null, "is_payout_account_setup_completed" boolean not null default false, "seller_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(
'alter table if exists "onboarding" add constraint "onboarding_seller_id_unique" unique ("seller_id");'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_onboarding_seller_id" ON "onboarding" (seller_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 "onboarding" add constraint "onboarding_seller_id_foreign" foreign key ("seller_id") references "seller" ("id") on update cascade;'
)
this.addSql('alter table if exists "member" drop column if exists "email";')
}
}

View File

@@ -0,0 +1,29 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20250225083755 extends Migration {
async up(): Promise<void> {
this.addSql('alter table if exists "seller" add column if not exists "address_line" text null, add column if not exists "city" text null, add column if not exists "postal_code" text null, add column if not exists "country_code" text null, add column if not exists "tax_id" text null;');
this.addSql('alter table if exists "member_invite" alter column "email" type text using ("email"::text);');
this.addSql('alter table if exists "member_invite" alter column "email" set not null;');
this.addSql('alter table if exists "member" alter column "email" type text using ("email"::text);');
this.addSql('alter table if exists "member" alter column "email" drop not null;');
}
async down(): Promise<void> {
this.addSql('alter table if exists "seller" drop column if exists "address_line";');
this.addSql('alter table if exists "seller" drop column if exists "city";');
this.addSql('alter table if exists "seller" drop column if exists "postal_code";');
this.addSql('alter table if exists "seller" drop column if exists "country_code";');
this.addSql('alter table if exists "seller" drop column if exists "tax_id";');
this.addSql('alter table if exists "member_invite" alter column "email" type text using ("email"::text);');
this.addSql('alter table if exists "member_invite" alter column "email" drop not null;');
this.addSql('alter table if exists "member" alter column "email" type text using ("email"::text);');
this.addSql('alter table if exists "member" alter column "email" set not null;');
}
}

View File

@@ -0,0 +1,26 @@
import { Migration } from '@mikro-orm/migrations'
export class Migration20250225094708 extends Migration {
async up(): Promise<void> {
this.addSql(
'create table if not exists "seller_onboarding" ("id" text not null, "store_information" boolean not null default false, "stripe_connection" boolean not null default false, "locations_shipping" boolean not null default false, "products" boolean not null default false, "seller_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "seller_onboarding_pkey" primary key ("id"));'
)
this.addSql(
'alter table if exists "seller_onboarding" add constraint "seller_onboarding_seller_id_unique" unique ("seller_id");'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_seller_onboarding_seller_id" ON "seller_onboarding" (seller_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_seller_onboarding_deleted_at" ON "seller_onboarding" (deleted_at) WHERE deleted_at IS NULL;'
)
this.addSql(
'alter table if exists "seller_onboarding" add constraint "seller_onboarding_seller_id_foreign" foreign key ("seller_id") references "seller" ("id") on update cascade;'
)
}
async down(): Promise<void> {
this.addSql('drop table if exists "seller_onboarding" cascade;')
}
}

View File

@@ -0,0 +1,15 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20250307091819 extends Migration {
async up(): Promise<void> {
this.addSql('alter table if exists "seller" add column if not exists "email" text null, add column if not exists "phone" text null, add column if not exists "state" text null;');
}
async down(): Promise<void> {
this.addSql('alter table if exists "seller" drop column if exists "email";');
this.addSql('alter table if exists "seller" drop column if exists "phone";');
this.addSql('alter table if exists "seller" drop column if exists "state";');
}
}

View File

@@ -0,0 +1,20 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20250313065552 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table if exists "seller_onboarding" drop constraint if exists "seller_onboarding_seller_id_unique";`);
this.addSql(`drop index if exists "seller_onboarding_seller_id_unique";`);
this.addSql(`drop index if exists "IDX_seller_onboarding_seller_id";`);
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_seller_onboarding_seller_id_unique" ON "seller_onboarding" (seller_id) WHERE deleted_at IS NULL;`);
}
override async down(): Promise<void> {
this.addSql(`drop index if exists "IDX_seller_onboarding_seller_id_unique";`);
this.addSql(`alter table if exists "seller_onboarding" add constraint "seller_onboarding_seller_id_unique" unique ("seller_id");`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_seller_onboarding_seller_id" ON "seller_onboarding" (seller_id) WHERE deleted_at IS NULL;`);
}
}

View File

@@ -0,0 +1,13 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20250530111528 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table if exists "seller" add column if not exists "store_status" text check ("store_status" in ('ACTIVE', 'INACTIVE', 'SUSPENDED')) not null default 'ACTIVE';`);
}
override async down(): Promise<void> {
this.addSql(`alter table if exists "seller" drop column if exists "store_status";`);
}
}

View File

@@ -0,0 +1,4 @@
export * from './seller'
export * from './member'
export * from './invite'
export * from './onboarding'

View File

@@ -0,0 +1,14 @@
import { model } from "@medusajs/framework/utils";
import { MemberRole } from "@mercurjs/framework";
import { Seller } from "./seller";
export const MemberInvite = model.define("member_invite", {
id: model.id({ prefix: "meminv" }).primaryKey(),
email: model.text(),
role: model.enum(MemberRole).default(MemberRole.OWNER),
seller: model.belongsTo(() => Seller, { mappedBy: "invites" }),
token: model.text(),
expires_at: model.dateTime(),
accepted: model.boolean().default(false),
});

View File

@@ -0,0 +1,15 @@
import { model } from "@medusajs/framework/utils";
import { MemberRole } from "@mercurjs/framework";
import { Seller } from "./seller";
export const Member = model.define("member", {
id: model.id({ prefix: "mem" }).primaryKey(),
role: model.enum(MemberRole).default(MemberRole.OWNER),
name: model.text().searchable(),
email: model.text().nullable(),
bio: model.text().searchable().nullable(),
phone: model.text().searchable().nullable(),
photo: model.text().nullable(),
seller: model.belongsTo(() => Seller, { mappedBy: "members" }),
});

View File

@@ -0,0 +1,12 @@
import { model } from '@medusajs/framework/utils'
import { Seller } from './seller'
export const SellerOnboarding = model.define('seller_onboarding', {
id: model.id({ prefix: 'sel_onb' }).primaryKey(),
store_information: model.boolean().default(false),
stripe_connection: model.boolean().default(false),
locations_shipping: model.boolean().default(false),
products: model.boolean().default(false),
seller: model.belongsTo(() => Seller, { mappedBy: 'onboarding' })
})

View File

@@ -0,0 +1,26 @@
import { model } from "@medusajs/framework/utils";
import { StoreStatus } from "@mercurjs/framework";
import { MemberInvite } from "./invite";
import { Member } from "./member";
import { SellerOnboarding } from "./onboarding";
export const Seller = model.define("seller", {
id: model.id({ prefix: "sel" }).primaryKey(),
store_status: model.enum(StoreStatus).default(StoreStatus.ACTIVE),
name: model.text().searchable(),
handle: model.text().unique(),
description: model.text().searchable().nullable(),
photo: model.text().nullable(),
email: model.text().nullable(),
phone: model.text().nullable(),
address_line: model.text().nullable(),
city: model.text().nullable(),
state: model.text().nullable(),
postal_code: model.text().nullable(),
country_code: model.text().nullable(),
tax_id: model.text().nullable(),
members: model.hasMany(() => Member),
invites: model.hasMany(() => MemberInvite),
onboarding: model.hasOne(() => SellerOnboarding).nullable(),
});

View File

@@ -0,0 +1,142 @@
import jwt, { JwtPayload } from "jsonwebtoken";
import { ConfigModule } from "@medusajs/framework";
import { Context, CreateInviteDTO } from "@medusajs/framework/types";
import {
InjectTransactionManager,
MedusaContext,
MedusaError,
MedusaService,
} from "@medusajs/framework/utils";
import { SELLER_MODULE } from ".";
import { Member, MemberInvite, Seller, SellerOnboarding } from "./models";
import { MemberInviteDTO } from "@mercurjs/framework";
type InjectedDependencies = {
configModule: ConfigModule;
};
type SellerModuleConfig = {
validInviteDuration: number;
};
// 7 days in ms
const DEFAULT_VALID_INVITE_DURATION = 1000 * 60 * 60 * 24 * 7;
class SellerModuleService extends MedusaService({
MemberInvite,
Member,
Seller,
SellerOnboarding,
}) {
private readonly config_: SellerModuleConfig;
private readonly httpConfig_: ConfigModule["projectConfig"]["http"];
constructor({ configModule }: InjectedDependencies) {
super(...arguments);
this.httpConfig_ = configModule.projectConfig.http;
const moduleDef = configModule.modules?.[SELLER_MODULE];
const options =
typeof moduleDef !== "boolean"
? (moduleDef?.options as SellerModuleConfig)
: null;
this.config_ = {
validInviteDuration:
options?.validInviteDuration ?? DEFAULT_VALID_INVITE_DURATION,
};
}
async validateInviteToken(token: string) {
const jwtSecret = this.httpConfig_.jwtSecret;
const decoded: JwtPayload = jwt.verify(token, jwtSecret, {
complete: true,
});
const invite = await this.retrieveMemberInvite(decoded.payload.id, {});
if (invite.accepted) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The invite has already been accepted"
);
}
if (invite.expires_at < new Date()) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The invite has expired"
);
}
return invite;
}
@InjectTransactionManager()
// @ts-expect-error: createInvites method already exists
async createMemberInvites(
input: CreateInviteDTO | CreateInviteDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<MemberInviteDTO[]> {
const data = Array.isArray(input) ? input : [input];
const expires_at = new Date();
expires_at.setMilliseconds(
new Date().getMilliseconds() + DEFAULT_VALID_INVITE_DURATION
);
const toCreate = data.map((invite) => {
return {
...invite,
expires_at: new Date(),
token: "placeholder",
};
});
const created = await super.createMemberInvites(toCreate, sharedContext);
const toUpdate = Array.isArray(created) ? created : [created];
const updates = toUpdate.map((invite) => {
return {
...invite,
id: invite.id,
expires_at,
token: this.generateToken({ id: invite.id }),
};
});
// @ts-ignore
await this.updateMemberInvites(updates, sharedContext);
return updates;
}
private generateToken(data: { id: string }): string {
const jwtSecret = this.httpConfig_.jwtSecret as string;
return jwt.sign(data, jwtSecret, {
expiresIn: this.config_.validInviteDuration / 1000,
});
}
async isOnboardingCompleted(seller_id: string): Promise<boolean> {
const { onboarding } = await this.retrieveSeller(seller_id, {
relations: ["onboarding"],
});
if (!onboarding) {
return false;
}
return (
onboarding.locations_shipping &&
onboarding.products &&
onboarding.store_information &&
onboarding.stripe_connection
);
}
}
export default SellerModuleService;

View File

@@ -0,0 +1,127 @@
import { MedusaContainer } from '@medusajs/framework'
import { ContainerRegistrationKeys } from '@medusajs/framework/utils'
export async function selectSellerCustomers(
container: MedusaContainer,
seller_id: string,
pagination: { skip: number; take: number },
fields: string[] = ['*']
) {
const query = container.resolve(ContainerRegistrationKeys.QUERY)
const knex = container.resolve(ContainerRegistrationKeys.PG_CONNECTION)
const customers = await knex
.select('id')
.from('customer')
.whereIn('customer.id', function () {
this.distinct('customer_id')
.from('order')
.leftJoin(
'seller_seller_order_order',
'order.id',
'seller_seller_order_order.order_id'
)
.where('seller_id', seller_id)
})
const { data, metadata } = await query.graph({
entity: 'customer',
fields,
filters: {
id: customers.map((c) => c.id)
},
pagination
})
return { customers: data, count: metadata?.count }
}
export async function selectCustomerOrders(
container: MedusaContainer,
seller_id: string,
customer_id: string,
pagination: { skip: number; take: number },
fields: string[] = ['*']
) {
const knex = container.resolve(ContainerRegistrationKeys.PG_CONNECTION)
const orders = await knex
.select(fields.map((f) => `order.${f}`))
.from('order')
.leftJoin(
'seller_seller_order_order',
'order.id',
'seller_seller_order_order.order_id'
)
.where('order.customer_id', customer_id)
.andWhere('seller_seller_order_order.seller_id', seller_id)
.limit(pagination.take)
.offset(pagination.skip)
const countResult = (await knex
.countDistinct('order.id')
.from('order')
.leftJoin(
'seller_seller_order_order',
'order.id',
'seller_seller_order_order.order_id'
)
.where('order.customer_id', customer_id)
.andWhere('seller_seller_order_order.seller_id', seller_id)) as {
count: string
}[]
return { orders, count: parseInt(countResult[0]?.count || '0') }
}
export async function selectOrdersChartData(
container: MedusaContainer,
seller_id: string,
time_range: [string, string]
): Promise<{ date: Date; count: string }[]> {
const knex = container.resolve(ContainerRegistrationKeys.PG_CONNECTION)
const result = await knex('seller_seller_order_order')
.select(knex.raw(`DATE_TRUNC('DAY', "created_at") AS date`))
.count('*')
.where('seller_id', seller_id)
.andWhereBetween('created_at', time_range)
.groupByRaw('date')
.orderByRaw('date asc')
return result as unknown as { date: Date; count: string }[]
}
export async function selectCustomersChartData(
container: MedusaContainer,
seller_id: string,
time_range: [string, string]
): Promise<{ date: Date; count: string }[]> {
const knex = container.resolve(ContainerRegistrationKeys.PG_CONNECTION)
const result = await knex
.with('customer_first_orders', (qb) => {
qb.select('customer_id')
.select(
knex.raw(
'MIN(seller_seller_order_order.created_at) as first_order_date'
)
)
.from('order')
.leftJoin(
'seller_seller_order_order',
'order.id',
'seller_seller_order_order.order_id'
)
.where('seller_id', seller_id)
.groupBy('customer_id')
})
.select(knex.raw(`DATE_TRUNC('DAY', "first_order_date") AS date`))
.count('*')
.from('customer_first_orders')
.whereBetween('first_order_date', time_range)
.groupByRaw('date')
.orderByRaw('date asc')
return result as unknown as { date: Date; count: string }[]
}

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"lib": ["ES2021"],
"target": "ES2021",
"outDir": "${configDir}/dist",
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"noUnusedLocals": true,
"module": "node16",
"moduleResolution": "node16",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"incremental": false
},
"include": ["${configDir}/src"],
"exclude": ["${configDir}/dist", "${configDir}/node_modules"]
}