chore(): Reorganize modules (#7210)

**What**
Move all modules to the modules directory
This commit is contained in:
Adrien de Peretti
2024-05-02 17:33:34 +02:00
committed by GitHub
parent 7a351eef09
commit 4eae25e1ef
870 changed files with 91 additions and 62 deletions

View File

@@ -0,0 +1,11 @@
import {
moduleDefinition,
revertMigration,
runMigrations,
} from "./module-definition"
export default moduleDefinition
export { revertMigration, runMigrations }
export * from "./initialize"
export * from "./types"

View File

@@ -0,0 +1,38 @@
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
MODULE_PACKAGE_NAMES,
Modules,
} from "@medusajs/modules-sdk"
import {
IPaymentModuleService,
ModuleProvider,
ModulesSdkTypes,
} from "@medusajs/types"
import { moduleDefinition } from "../module-definition"
import { InitializeModuleInjectableDependencies } from "../types"
export const initialize = async (
options?:
| (
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
| ExternalModuleDeclaration
| InternalModuleDeclaration
) & { providers: ModuleProvider[] },
injectedDependencies?: InitializeModuleInjectableDependencies
): Promise<IPaymentModuleService> => {
const loaded = await MedusaModule.bootstrap<IPaymentModuleService>({
moduleKey: Modules.PAYMENT,
defaultPath: MODULE_PACKAGE_NAMES[Modules.PAYMENT],
declaration: options as
| InternalModuleDeclaration
| ExternalModuleDeclaration,
injectedDependencies,
moduleExports: moduleDefinition,
})
return loaded[Modules.PAYMENT]
}

View File

@@ -0,0 +1,61 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { MapToConfig } from "@medusajs/utils"
import {
Payment,
PaymentCollection,
PaymentProvider,
PaymentSession,
} from "@models"
export const LinkableKeys = {
payment_id: Payment.name,
payment_collection_id: PaymentCollection.name,
payment_provider_id: PaymentProvider.name,
}
const entityLinkableKeysMap: MapToConfig = {}
Object.entries(LinkableKeys).forEach(([key, value]) => {
entityLinkableKeysMap[value] ??= []
entityLinkableKeysMap[value].push({
mapTo: key,
valueFrom: key.split("_").pop()!,
})
})
export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap
export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.PAYMENT,
primaryKeys: ["id"],
linkableKeys: LinkableKeys,
alias: [
{
name: ["payment", "payments"],
args: {
entity: Payment.name,
methodSuffix: "Payments",
},
},
{
name: ["payment_collection", "payment_collections"],
args: {
entity: PaymentCollection.name,
},
},
{
name: ["payment_session", "payment_sessions"],
args: {
entity: PaymentSession.name,
methodSuffix: "PaymentSessions",
},
},
{
name: ["payment_provider", "payment_providers"],
args: {
entity: PaymentProvider.name,
methodSuffix: "PaymentProviders",
},
},
],
}

View File

@@ -0,0 +1,34 @@
import {
InternalModuleDeclaration,
LoaderOptions,
Modules,
} from "@medusajs/modules-sdk"
import { ModulesSdkTypes } from "@medusajs/types"
import { ModulesSdkUtils } from "@medusajs/utils"
import { EntitySchema } from "@mikro-orm/core"
import * as PaymentModels from "../models"
export default async (
{
options,
container,
logger,
}: LoaderOptions<
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
>,
moduleDeclaration?: InternalModuleDeclaration
): Promise<void> => {
const entities = Object.values(PaymentModels) as unknown as EntitySchema[]
const pathToMigrations = __dirname + "/../migrations"
await ModulesSdkUtils.mikroOrmConnectionLoader({
moduleName: Modules.PAYMENT,
entities,
container,
options,
moduleDeclaration,
logger,
pathToMigrations,
})
}

View File

@@ -0,0 +1,10 @@
import { ModulesSdkUtils } from "@medusajs/utils"
import * as ModuleModels from "@models"
import * as ModuleRepositories from "@repositories"
import * as ModuleServices from "@services"
export default ModulesSdkUtils.moduleContainerLoaderFactory({
moduleModels: ModuleModels,
moduleRepositories: ModuleRepositories,
moduleServices: ModuleServices,
})

View File

@@ -0,0 +1,26 @@
import {
CreatePaymentProviderDTO,
LoaderOptions
} from "@medusajs/types"
export default async ({ container }: LoaderOptions): Promise<void> => {
const providersToLoad = container.resolve("payment_providers")
const paymentProviderService = container.resolve("paymentProviderService")
const providers = await paymentProviderService.list({
id: providersToLoad,
})
const loadedProvidersMap = new Map(providers.map((p) => [p.id, p]))
const providersToCreate: CreatePaymentProviderDTO[] = []
for (const id of providersToLoad) {
if (loadedProvidersMap.has(id)) {
continue
}
providersToCreate.push({ id })
}
await paymentProviderService.create(providersToCreate)
}

View File

@@ -0,0 +1,4 @@
export * from "./connection"
export * from "./container"
export * from "./providers"
export * from "./defaults"

View File

@@ -0,0 +1,40 @@
import { moduleProviderLoader } from "@medusajs/modules-sdk"
import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types"
import { Lifetime, asFunction, asValue } from "awilix"
import * as providers from "../providers"
const registrationFn = async (klass, container, pluginOptions) => {
Object.entries(pluginOptions.config || []).map(([name, config]) => {
const key = `pp_${klass.PROVIDER}_${name}`
container.register({
[key]: asFunction((cradle) => new klass(cradle, config), {
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
}),
})
container.registerAdd("payment_providers", asValue(key))
})
}
export default async ({
container,
options,
}: LoaderOptions<
(
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
) & { providers: ModuleProvider[] }
>): Promise<void> => {
// Local providers
for (const provider of Object.values(providers)) {
await registrationFn(provider, container, { config: { default: {} } })
}
await moduleProviderLoader({
container,
providers: options?.providers || [],
registerServiceFn: registrationFn,
})
}

View File

@@ -0,0 +1,262 @@
import { generatePostgresAlterColummnIfExistStatement } from "@medusajs/utils"
import { Migration } from "@mikro-orm/migrations"
export class Migration20240225134525 extends Migration {
async up(): Promise<void> {
const paymentCollectionExists = await this.execute(
`SELECT * FROM information_schema.tables where table_name = 'payment_collection' and table_schema = 'public';`
)
if (paymentCollectionExists.length) {
this.addSql(`
${generatePostgresAlterColummnIfExistStatement(
"payment_collection",
["type", "created_by"],
"DROP NOT NULL"
)}
ALTER TABLE IF EXISTS "payment_collection" ADD COLUMN IF NOT EXISTS "completed_at" TIMESTAMPTZ NULL;
ALTER TABLE IF EXISTS "payment_collection" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
ALTER TABLE IF EXISTS "payment_collection" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
ALTER TABLE "payment_collection" DROP CONSTRAINT "FK_payment_collection_region_id";
ALTER TABLE IF EXISTS "payment_provider" ADD COLUMN IF NOT EXISTS "is_enabled" BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "payment_collection_id" TEXT NOT NULL;
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "currency_code" TEXT NOT NULL;
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "authorized_at" TEXT NULL;
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "payment_authorized_at" TIMESTAMPTZ NULL;
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "context" JSONB NULL;
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "payment_collection_id" TEXT NOT NULL;
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "provider_id" TEXT NOT NULL;
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "payment_session_id" TEXT NOT NULL;
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "customer_id" TEXT NULL;
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "created_by" TEXT NULL;
${generatePostgresAlterColummnIfExistStatement(
"refund",
["reason"],
"DROP NOT NULL"
)}
CREATE TABLE IF NOT EXISTS "capture" (
"id" TEXT NOT NULL,
"amount" NUMERIC NOT NULL,
"raw_amount" JSONB NOT NULL,
"payment_id" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"deleted_at" TIMESTAMPTZ NULL,
"created_by" TEXT NULL,
"metadata" JSONB NULL,
CONSTRAINT "capture_pkey" PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "payment_method_token" (
"id" TEXT NOT NULL,
"provider_id" TEXT NOT NULL,
"data" JSONB NULL,
"name" TEXT NOT NULL,
"type_detail" TEXT NULL,
"description_detail" TEXT NULL,
"metadata" JSONB NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"deleted_at" TIMESTAMPTZ NULL,
CONSTRAINT "payment_method_token_pkey" PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "payment_collection_payment_providers" (
"payment_collection_id" TEXT NOT NULL,
"payment_provider_id" TEXT NOT NULL,
CONSTRAINT "payment_collection_payment_providers_pkey" PRIMARY KEY ("payment_collection_id", "payment_provider_id")
);
ALTER TABLE IF EXISTS "payment_collection_payment_providers"
ADD CONSTRAINT "payment_collection_payment_providers_payment_coll_aa276_foreign" FOREIGN KEY ("payment_collection_id") REFERENCES "payment_collection" ("id") ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE IF EXISTS "payment_collection_payment_providers"
ADD CONSTRAINT "payment_collection_payment_providers_payment_provider_id_foreign" FOREIGN KEY ("payment_provider_id") REFERENCES "payment_provider" ("id") ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE IF EXISTS "capture"
ADD CONSTRAINT "capture_payment_id_foreign" FOREIGN KEY ("payment_id") REFERENCES "payment" ("id") ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE IF EXISTS "refund"
ADD CONSTRAINT "refund_payment_id_foreign" FOREIGN KEY ("payment_id") REFERENCES "payment" ("id") ON UPDATE CASCADE ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS "IDX_payment_deleted_at" ON "payment" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_payment_collection_id" ON "payment" ("payment_collection_id") WHERE "deleted_at" IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_method_token_deleted_at" ON "payment_method_token" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_provider_id" ON "payment" ("provider_id") WHERE "deleted_at" IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_collection_region_id" ON "payment_collection" ("region_id") WHERE "deleted_at" IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_collection_deleted_at" ON "payment_collection" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_refund_payment_id" ON "refund" ("payment_id") WHERE "deleted_at" IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_refund_deleted_at" ON "payment" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_capture_payment_id" ON "capture" ("payment_id") WHERE "deleted_at" IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_capture_deleted_at" ON "payment" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_session_payment_collection_id" ON "payment_session" ("payment_collection_id") WHERE "deleted_at" IS NULL;
`)
} else {
this.addSql(`
CREATE TABLE IF NOT EXISTS "payment_collection" (
"id" TEXT NOT NULL,
"currency_code" TEXT NOT NULL,
"amount" NUMERIC NOT NULL,
"raw_amount" JSONB NOT NULL,
"region_id" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"deleted_at" TIMESTAMPTZ NULL,
"completed_at" TIMESTAMPTZ NULL,
"status" TEXT CHECK ("status" IN ('not_paid', 'awaiting', 'authorized', 'partially_authorized', 'canceled')) NOT NULL DEFAULT 'not_paid',
"metadata" JSONB NULL,
CONSTRAINT "payment_collection_pkey" PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "payment_method_token" (
"id" TEXT NOT NULL,
"provider_id" TEXT NOT NULL,
"data" JSONB NULL,
"name" TEXT NOT NULL,
"type_detail" TEXT NULL,
"description_detail" TEXT NULL,
"metadata" JSONB NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"deleted_at" TIMESTAMPTZ NULL,
CONSTRAINT "payment_method_token_pkey" PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "payment_provider" (
"id" TEXT NOT NULL,
"is_enabled" BOOLEAN NOT NULL DEFAULT TRUE,
CONSTRAINT "payment_provider_pkey" PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "payment_collection_payment_providers" (
"payment_collection_id" TEXT NOT NULL,
"payment_provider_id" TEXT NOT NULL,
CONSTRAINT "payment_collection_payment_providers_pkey" PRIMARY KEY ("payment_collection_id", "payment_provider_id")
);
CREATE TABLE IF NOT EXISTS "payment_session" (
"id" TEXT NOT NULL,
"currency_code" TEXT NOT NULL,
"amount" NUMERIC NOT NULL,
"raw_amount" JSONB NOT NULL,
"provider_id" TEXT NOT NULL,
"data" JSONB NOT NULL,
"context" JSONB NULL,
"status" TEXT CHECK ("status" IN ('authorized', 'pending', 'requires_more', 'error', 'canceled')) NOT NULL DEFAULT 'pending',
"authorized_at" TIMESTAMPTZ NULL,
"payment_collection_id" TEXT NOT NULL,
"metadata" JSONB NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"deleted_at" TIMESTAMPTZ NULL,
CONSTRAINT "payment_session_pkey" PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "payment" (
"id" TEXT NOT NULL,
"amount" NUMERIC NOT NULL,
"raw_amount" JSONB NOT NULL,
"currency_code" TEXT NOT NULL,
"provider_id" TEXT NOT NULL,
"cart_id" TEXT NULL,
"order_id" TEXT NULL,
"customer_id" TEXT NULL,
"data" JSONB NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"deleted_at" TIMESTAMPTZ NULL,
"captured_at" TIMESTAMPTZ NULL,
"canceled_at" TIMESTAMPTZ NULL,
"payment_collection_id" TEXT NOT NULL,
"payment_session_id" TEXT NOT NULL,
"metadata" JSONB NULL,
CONSTRAINT "payment_pkey" PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "refund" (
"id" TEXT NOT NULL,
"amount" NUMERIC NOT NULL,
"raw_amount" JSONB NOT NULL,
"payment_id" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"deleted_at" TIMESTAMPTZ NULL,
"created_by" TEXT NULL,
"metadata" JSONB NULL,
CONSTRAINT "refund_pkey" PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "capture" (
"id" TEXT NOT NULL,
"amount" NUMERIC NOT NULL,
"raw_amount" JSONB NOT NULL,
"payment_id" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"deleted_at" TIMESTAMPTZ NULL,
"created_by" TEXT NULL,
"metadata" JSONB NULL,
CONSTRAINT "capture_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "IDX_payment_deleted_at" ON "payment" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_payment_collection_id" ON "payment" ("payment_collection_id") WHERE "deleted_at" IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_method_token_deleted_at" ON "payment_method_token" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_provider_id" ON "payment" ("provider_id") WHERE "deleted_at" IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_collection_region_id" ON "payment_collection" ("region_id") WHERE "deleted_at" IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_collection_deleted_at" ON "payment_collection" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_refund_payment_id" ON "refund" ("payment_id") WHERE "deleted_at" IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_refund_deleted_at" ON "payment" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_capture_payment_id" ON "capture" ("payment_id") WHERE "deleted_at" IS NULL;
CREATE INDEX IF NOT EXISTS "IDX_capture_deleted_at" ON "payment" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
CREATE INDEX IF NOT EXISTS "IDX_payment_session_payment_collection_id" ON "payment_session" ("payment_collection_id") WHERE "deleted_at" IS NULL;
ALTER TABLE IF EXISTS "payment_collection_payment_providers"
ADD CONSTRAINT "payment_collection_payment_providers_payment_coll_aa276_foreign" FOREIGN KEY ("payment_collection_id") REFERENCES "payment_collection" ("id") ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE IF EXISTS "payment_collection_payment_providers"
ADD CONSTRAINT "payment_collection_payment_providers_payment_provider_id_foreign" FOREIGN KEY ("payment_provider_id") REFERENCES "payment_provider" ("id") ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE IF EXISTS "payment_session"
ADD CONSTRAINT "payment_session_payment_collection_id_foreign" FOREIGN KEY ("payment_collection_id") REFERENCES "payment_collection" ("id") ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE IF EXISTS "payment"
ADD CONSTRAINT "payment_payment_collection_id_foreign" FOREIGN KEY ("payment_collection_id") REFERENCES "payment_collection" ("id") ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE IF EXISTS "capture"
ADD CONSTRAINT "capture_payment_id_foreign" FOREIGN KEY ("payment_id") REFERENCES "payment" ("id") ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE IF EXISTS "refund"
ADD CONSTRAINT "refund_payment_id_foreign" FOREIGN KEY ("payment_id") REFERENCES "payment" ("id") ON UPDATE CASCADE ON DELETE CASCADE;
`)
}
}
}

View File

@@ -0,0 +1,74 @@
import { BigNumberRawValue } from "@medusajs/types"
import {
BigNumber,
MikroOrmBigNumberProperty,
generateEntityId,
} from "@medusajs/utils"
import {
BeforeCreate,
Entity,
ManyToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Payment from "./payment"
type OptionalCaptureProps = "created_at"
@Entity({ tableName: "capture" })
export default class Capture {
[OptionalProps]?: OptionalCaptureProps
@PrimaryKey({ columnType: "text" })
id: string
@MikroOrmBigNumberProperty()
amount: BigNumber | number
@Property({ columnType: "jsonb" })
raw_amount: BigNumberRawValue
@ManyToOne(() => Payment, {
onDelete: "cascade",
index: "IDX_capture_payment_id",
fieldName: "payment_id",
})
payment!: Payment
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({
columnType: "timestamptz",
nullable: true,
index: "IDX_capture_deleted_at",
})
deleted_at: Date | null = null
@Property({ columnType: "text", nullable: true })
created_by: string | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "capt")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "capt")
}
}

View File

@@ -0,0 +1,7 @@
export { default as Capture } from "./capture"
export { default as Payment } from "./payment"
export { default as PaymentCollection } from "./payment-collection"
export { default as PaymentMethodToken } from "./payment-method-token"
export { default as PaymentProvider } from "./payment-provider"
export { default as PaymentSession } from "./payment-session"
export { default as Refund } from "./refund"

View File

@@ -0,0 +1,108 @@
import { BigNumberRawValue, DAL } from "@medusajs/types"
import {
BigNumber,
DALUtils,
MikroOrmBigNumberProperty,
PaymentCollectionStatus,
generateEntityId,
} from "@medusajs/utils"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Enum,
Filter,
ManyToMany,
OnInit,
OneToMany,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Payment from "./payment"
import PaymentProvider from "./payment-provider"
import PaymentSession from "./payment-session"
type OptionalPaymentCollectionProps = "status" | DAL.EntityDateColumns
@Entity({ tableName: "payment_collection" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class PaymentCollection {
[OptionalProps]?: OptionalPaymentCollectionProps
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
currency_code: string
@MikroOrmBigNumberProperty()
amount: BigNumber | number
@Property({ columnType: "jsonb" })
raw_amount: BigNumberRawValue
@Property({ columnType: "text", index: "IDX_payment_collection_region_id" })
region_id: string
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({
columnType: "timestamptz",
nullable: true,
index: "IDX_payment_collection_deleted_at",
})
deleted_at: Date | null = null
@Property({
columnType: "timestamptz",
nullable: true,
})
completed_at: Date | null = null
@Enum({
items: () => PaymentCollectionStatus,
default: PaymentCollectionStatus.NOT_PAID,
})
status: PaymentCollectionStatus = PaymentCollectionStatus.NOT_PAID
@ManyToMany(() => PaymentProvider)
payment_providers = new Collection<PaymentProvider>(this)
@OneToMany(() => PaymentSession, (ps) => ps.payment_collection, {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
})
payment_sessions = new Collection<PaymentSession>(this)
@OneToMany(() => Payment, (payment) => payment.payment_collection, {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
})
payments = new Collection<Payment>(this)
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "pay_col")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "pay_col")
}
}

View File

@@ -0,0 +1,64 @@
import { generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Entity,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
@Entity({ tableName: "payment_method_token" })
export default class PaymentMethodToken {
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
provider_id: string
@Property({ columnType: "jsonb", nullable: true })
data: Record<string, unknown> | null = null
@Property({ columnType: "text" })
name: string
@Property({ columnType: "text", nullable: true })
type_detail: string | null = null
@Property({ columnType: "text", nullable: true })
description_detail: string | null = null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({
columnType: "timestamptz",
nullable: true,
index: "IDX_payment_metod_token_deleted_at",
})
deleted_at: Date | null = null
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "paymttok")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "paymttok")
}
}

View File

@@ -0,0 +1,15 @@
import { Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"
@Entity({ tableName: "payment_provider" })
export default class PaymentProvider {
[OptionalProps]?: "is_enabled"
@PrimaryKey({ columnType: "text" })
id: string
@Property({
default: true,
columnType: "boolean",
})
is_enabled: boolean = true
}

View File

@@ -0,0 +1,112 @@
import { BigNumberRawValue } from "@medusajs/types"
import {
BigNumber,
generateEntityId,
MikroOrmBigNumberProperty,
PaymentSessionStatus,
} from "@medusajs/utils"
import {
BeforeCreate,
Entity,
Enum,
ManyToOne,
OneToOne,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Payment from "./payment"
import PaymentCollection from "./payment-collection"
@Entity({ tableName: "payment_session" })
export default class PaymentSession {
[OptionalProps]?: "status" | "data"
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
currency_code: string
@MikroOrmBigNumberProperty()
amount: BigNumber | number
@Property({
columnType: "jsonb",
})
raw_amount: BigNumberRawValue
@Property({ columnType: "text" })
provider_id: string
@Property({ columnType: "jsonb" })
data: Record<string, unknown> = {}
@Property({ columnType: "jsonb", nullable: true })
context: Record<string, unknown> | null
@Enum({
items: () => PaymentSessionStatus,
})
status: PaymentSessionStatus = PaymentSessionStatus.PENDING
@Property({
columnType: "timestamptz",
nullable: true,
})
authorized_at: Date | null = null
@ManyToOne(() => PaymentCollection, {
persist: false,
})
payment_collection: PaymentCollection
@ManyToOne({
entity: () => PaymentCollection,
columnType: "text",
index: "IDX_payment_session_payment_collection_id",
fieldName: "payment_collection_id",
mapToPk: true,
})
payment_collection_id: string
@OneToOne({
entity: () => Payment,
nullable: true,
mappedBy: "payment_session",
})
payment?: Payment | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({
columnType: "timestamptz",
nullable: true,
index: "IDX_payment_session_deleted_at",
})
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "payses")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "payses")
}
}

View File

@@ -0,0 +1,150 @@
import { BigNumberRawValue, DAL } from "@medusajs/types"
import {
BigNumber,
DALUtils,
MikroOrmBigNumberProperty,
Searchable,
generateEntityId,
} from "@medusajs/utils"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
ManyToOne,
OnInit,
OneToMany,
OneToOne,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Capture from "./capture"
import PaymentCollection from "./payment-collection"
import PaymentSession from "./payment-session"
import Refund from "./refund"
type OptionalPaymentProps = DAL.EntityDateColumns
@Entity({ tableName: "payment" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class Payment {
[OptionalProps]?: OptionalPaymentProps
@PrimaryKey({ columnType: "text" })
id: string
@MikroOrmBigNumberProperty()
amount: BigNumber | number
@Property({ columnType: "jsonb" })
raw_amount: BigNumberRawValue
@Property({ columnType: "text" })
currency_code: string
@Property({ columnType: "text" })
provider_id: string
@Searchable()
@Property({ columnType: "text", nullable: true })
cart_id: string | null = null
@Searchable()
@Property({ columnType: "text", nullable: true })
order_id: string | null = null
@Searchable()
@Property({ columnType: "text", nullable: true })
customer_id: string | null = null
@Property({ columnType: "jsonb", nullable: true })
data: Record<string, unknown> | null = null
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({
columnType: "timestamptz",
nullable: true,
index: "IDX_payment_deleted_at",
})
deleted_at: Date | null = null
@Property({
columnType: "timestamptz",
nullable: true,
})
captured_at: Date | null = null
@Property({
columnType: "timestamptz",
nullable: true,
})
canceled_at: Date | null = null
@OneToMany(() => Refund, (refund) => refund.payment, {
cascade: [Cascade.REMOVE],
})
refunds = new Collection<Refund>(this)
@OneToMany(() => Capture, (capture) => capture.payment, {
cascade: [Cascade.REMOVE],
})
captures = new Collection<Capture>(this)
@ManyToOne({
entity: () => PaymentCollection,
persist: false,
})
payment_collection: PaymentCollection
@ManyToOne({
entity: () => PaymentCollection,
columnType: "text",
index: "IDX_payment_payment_collection_id",
fieldName: "payment_collection_id",
mapToPk: true,
})
payment_collection_id: string
@OneToOne({
owner: true,
fieldName: "payment_session_id",
index: "IDX_payment_payment_session_id",
})
payment_session: PaymentSession
/** COMPUTED PROPERTIES START **/
captured_amount: number // sum of the associated captures
refunded_amount: number // sum of the associated refunds
/** COMPUTED PROPERTIES END **/
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "pay")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "pay")
}
}

View File

@@ -0,0 +1,71 @@
import { BigNumberRawValue } from "@medusajs/types"
import {
BigNumber,
MikroOrmBigNumberProperty,
generateEntityId,
} from "@medusajs/utils"
import {
BeforeCreate,
Entity,
ManyToOne,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Payment from "./payment"
@Entity({ tableName: "refund" })
export default class Refund {
@PrimaryKey({ columnType: "text" })
id: string
@MikroOrmBigNumberProperty()
amount: BigNumber | number
@Property({ columnType: "jsonb" })
raw_amount: BigNumberRawValue
@ManyToOne(() => Payment, {
onDelete: "cascade",
index: "IDX_refund_payment_id",
fieldName: "payment_id",
})
payment!: Payment
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({
columnType: "timestamptz",
nullable: true,
index: "IDX_refund_deleted_at",
})
deleted_at: Date | null = null
@Property({ columnType: "text", nullable: true })
created_by: string | null = null
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "ref")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "ref")
}
}

View File

@@ -0,0 +1,41 @@
import { ModuleExports } from "@medusajs/types"
import { PaymentModuleService } from "@services"
import loadConnection from "./loaders/connection"
import loadContainer from "./loaders/container"
import loadProviders from "./loaders/providers"
import loadDefaults from "./loaders/defaults"
import { Modules } from "@medusajs/modules-sdk"
import { ModulesSdkUtils } from "@medusajs/utils"
import * as PaymentModels from "@models"
const migrationScriptOptions = {
moduleName: Modules.PAYMENT,
models: PaymentModels,
pathToMigrations: __dirname + "/migrations",
}
export const runMigrations = ModulesSdkUtils.buildMigrationScript(
migrationScriptOptions
)
export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript(
migrationScriptOptions
)
const service = PaymentModuleService
const loaders = [
loadContainer,
loadConnection,
loadProviders,
loadDefaults,
] as any
export const moduleDefinition: ModuleExports = {
service,
loaders,
runMigrations,
revertMigration,
}

View File

@@ -0,0 +1 @@
export { default as SystemPaymentProvider } from "./system"

View File

@@ -0,0 +1,80 @@
import {
CreatePaymentProviderSession,
PaymentProviderError,
PaymentProviderSessionResponse,
PaymentSessionStatus,
ProviderWebhookPayload,
WebhookActionResult,
} from "@medusajs/types"
import { AbstractPaymentProvider, PaymentActions } from "@medusajs/utils"
export class SystemProviderService extends AbstractPaymentProvider {
static identifier = "system"
static PROVIDER = "system"
async getStatus(_): Promise<string> {
return "authorized"
}
async getPaymentData(_): Promise<Record<string, unknown>> {
return {}
}
async initiatePayment(
context: CreatePaymentProviderSession
): Promise<PaymentProviderSessionResponse> {
return { data: {} }
}
async getPaymentStatus(
paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus> {
throw new Error("Method not implemented.")
}
async retrievePayment(
paymentSessionData: Record<string, unknown>
): Promise<Record<string, unknown> | PaymentProviderError> {
return {}
}
async authorizePayment(_): Promise<
| PaymentProviderError
| {
status: PaymentSessionStatus
data: PaymentProviderSessionResponse["data"]
}
> {
return { data: {}, status: PaymentSessionStatus.AUTHORIZED }
}
async updatePayment(
_
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
return { data: {} } as PaymentProviderSessionResponse
}
async deletePayment(_): Promise<Record<string, unknown>> {
return {}
}
async capturePayment(_): Promise<Record<string, unknown>> {
return {}
}
async refundPayment(_): Promise<Record<string, unknown>> {
return {}
}
async cancelPayment(_): Promise<Record<string, unknown>> {
return {}
}
async getWebhookActionAndData(
data: ProviderWebhookPayload["payload"]
): Promise<WebhookActionResult> {
return { action: PaymentActions.NOT_SUPPORTED }
}
}
export default SystemProviderService

View File

@@ -0,0 +1 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env node
import { EOL } from "os"
import { ModulesSdkUtils } from "@medusajs/utils"
import { Modules } from "@medusajs/modules-sdk"
import * as PaymentModels from "@models"
import { createPayments } from "../seed-utils"
const args = process.argv
const path = args.pop() as string
export default (async () => {
const { config } = await import("dotenv")
config()
if (!path) {
throw new Error(
`filePath is required.${EOL}Example: medusa-payment-seed <filePath>`
)
}
const run = ModulesSdkUtils.buildSeedScript({
moduleName: Modules.PAYMENT,
models: PaymentModels,
pathToMigrations: __dirname + "/../../migrations",
seedHandler: async ({ manager, data }) => {
const { paymentsData } = data
await createPayments(manager, paymentsData)
},
})
await run({ path })
})()

View File

@@ -0,0 +1,8 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
export async function createPayments(
manager: SqlEntityManager,
paymentsData: any[]
): Promise<any[]> {
return []
}

View File

@@ -0,0 +1,3 @@
export { default as PaymentModuleService } from "./payment-module"
export { default as PaymentProviderService } from "./payment-provider"

View File

@@ -0,0 +1,759 @@
import {
CaptureDTO,
Context,
CreateCaptureDTO,
CreatePaymentCollectionDTO,
CreatePaymentSessionDTO,
CreateRefundDTO,
DAL,
FilterablePaymentCollectionProps,
FilterablePaymentProviderProps,
FindConfig,
InternalModuleDeclaration,
IPaymentModuleService,
ModuleJoinerConfig,
ModulesSdkTypes,
PaymentCollectionDTO,
PaymentCollectionUpdatableFields,
PaymentDTO,
PaymentProviderDTO,
PaymentSessionDTO,
PaymentSessionStatus,
ProviderWebhookPayload,
RefundDTO,
UpdatePaymentCollectionDTO,
UpdatePaymentDTO,
UpdatePaymentSessionDTO,
UpsertPaymentCollectionDTO,
} from "@medusajs/types"
import {
BigNumber,
InjectManager,
InjectTransactionManager,
isString,
MathBN,
MedusaContext,
MedusaError,
ModulesSdkUtils,
PaymentActions,
promiseAll,
} from "@medusajs/utils"
import {
Capture,
Payment,
PaymentCollection,
PaymentSession,
Refund,
} from "@models"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import PaymentProviderService from "./payment-provider"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
paymentService: ModulesSdkTypes.InternalModuleService<any>
captureService: ModulesSdkTypes.InternalModuleService<any>
refundService: ModulesSdkTypes.InternalModuleService<any>
paymentSessionService: ModulesSdkTypes.InternalModuleService<any>
paymentCollectionService: ModulesSdkTypes.InternalModuleService<any>
paymentProviderService: PaymentProviderService
}
const generateMethodForModels = [
PaymentCollection,
Payment,
PaymentSession,
Capture,
Refund,
]
export default class PaymentModuleService<
TPaymentCollection extends PaymentCollection = PaymentCollection,
TPayment extends Payment = Payment,
TCapture extends Capture = Capture,
TRefund extends Refund = Refund,
TPaymentSession extends PaymentSession = PaymentSession
>
extends ModulesSdkUtils.abstractModuleServiceFactory<
InjectedDependencies,
PaymentCollectionDTO,
{
PaymentCollection: { dto: PaymentCollectionDTO }
PaymentSession: { dto: PaymentSessionDTO }
Payment: { dto: PaymentDTO }
Capture: { dto: CaptureDTO }
Refund: { dto: RefundDTO }
}
>(PaymentCollection, generateMethodForModels, entityNameToLinkableKeysMap)
implements IPaymentModuleService
{
protected baseRepository_: DAL.RepositoryService
protected paymentService_: ModulesSdkTypes.InternalModuleService<TPayment>
protected captureService_: ModulesSdkTypes.InternalModuleService<TCapture>
protected refundService_: ModulesSdkTypes.InternalModuleService<TRefund>
protected paymentSessionService_: ModulesSdkTypes.InternalModuleService<TPaymentSession>
protected paymentCollectionService_: ModulesSdkTypes.InternalModuleService<TPaymentCollection>
protected paymentProviderService_: PaymentProviderService
constructor(
{
baseRepository,
paymentService,
captureService,
refundService,
paymentSessionService,
paymentProviderService,
paymentCollectionService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
this.refundService_ = refundService
this.captureService_ = captureService
this.paymentService_ = paymentService
this.paymentSessionService_ = paymentSessionService
this.paymentProviderService_ = paymentProviderService
this.paymentCollectionService_ = paymentCollectionService
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
createPaymentCollections(
data: CreatePaymentCollectionDTO,
sharedContext?: Context
): Promise<PaymentCollectionDTO>
createPaymentCollections(
data: CreatePaymentCollectionDTO[],
sharedContext?: Context
): Promise<PaymentCollectionDTO[]>
@InjectManager("baseRepository_")
async createPaymentCollections(
data: CreatePaymentCollectionDTO | CreatePaymentCollectionDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
const input = Array.isArray(data) ? data : [data]
const collections = await this.createPaymentCollections_(
input,
sharedContext
)
return await this.baseRepository_.serialize<PaymentCollectionDTO[]>(
Array.isArray(data) ? collections : collections[0],
{
populate: true,
}
)
}
@InjectTransactionManager("baseRepository_")
async createPaymentCollections_(
data: CreatePaymentCollectionDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentCollection[]> {
return this.paymentCollectionService_.create(data, sharedContext)
}
updatePaymentCollections(
paymentCollectionId: string,
data: PaymentCollectionUpdatableFields,
sharedContext?: Context
): Promise<PaymentCollectionDTO>
updatePaymentCollections(
selector: FilterablePaymentCollectionProps,
data: PaymentCollectionUpdatableFields,
sharedContext?: Context
): Promise<PaymentCollectionDTO[]>
@InjectManager("baseRepository_")
async updatePaymentCollections(
idOrSelector: string | FilterablePaymentCollectionProps,
data: PaymentCollectionUpdatableFields,
@MedusaContext() sharedContext?: Context
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
let updateData: UpdatePaymentCollectionDTO[] = []
if (isString(idOrSelector)) {
updateData = [
{
id: idOrSelector,
...data,
},
]
} else {
const collections = await this.paymentCollectionService_.list(
idOrSelector,
{},
sharedContext
)
updateData = collections.map((c) => ({
id: c.id,
...data,
}))
}
const result = await this.updatePaymentCollections_(
updateData,
sharedContext
)
return await this.baseRepository_.serialize<PaymentCollectionDTO[]>(
Array.isArray(data) ? result : result[0],
{
populate: true,
}
)
}
@InjectTransactionManager("baseRepository_")
async updatePaymentCollections_(
data: UpdatePaymentCollectionDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentCollection[]> {
return await this.paymentCollectionService_.update(data, sharedContext)
}
upsertPaymentCollections(
data: UpsertPaymentCollectionDTO[],
sharedContext?: Context
): Promise<PaymentCollectionDTO[]>
upsertPaymentCollections(
data: UpsertPaymentCollectionDTO,
sharedContext?: Context
): Promise<PaymentCollectionDTO>
@InjectTransactionManager("baseRepository_")
async upsertPaymentCollections(
data: UpsertPaymentCollectionDTO | UpsertPaymentCollectionDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter(
(collection): collection is UpdatePaymentCollectionDTO => !!collection.id
)
const forCreate = input.filter(
(collection): collection is CreatePaymentCollectionDTO => !collection.id
)
const operations: Promise<PaymentCollection[]>[] = []
if (forCreate.length) {
operations.push(this.createPaymentCollections_(forCreate, sharedContext))
}
if (forUpdate.length) {
operations.push(this.updatePaymentCollections_(forUpdate, sharedContext))
}
const result = (await promiseAll(operations)).flat()
return await this.baseRepository_.serialize<
PaymentCollectionDTO[] | PaymentCollectionDTO
>(Array.isArray(data) ? result : result[0])
}
completePaymentCollections(
paymentCollectionId: string,
sharedContext?: Context
): Promise<PaymentCollectionDTO>
completePaymentCollections(
paymentCollectionId: string[],
sharedContext?: Context
): Promise<PaymentCollectionDTO[]>
@InjectTransactionManager("baseRepository_")
async completePaymentCollections(
paymentCollectionId: string | string[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
const input = Array.isArray(paymentCollectionId)
? paymentCollectionId.map((id) => ({
id,
completed_at: new Date(),
}))
: [{ id: paymentCollectionId, completed_at: new Date() }]
// TODO: what checks should be done here? e.g. captured_amount === amount?
const updated = await this.paymentCollectionService_.update(
input,
sharedContext
)
return await this.baseRepository_.serialize(
Array.isArray(paymentCollectionId) ? updated : updated[0],
{ populate: true }
)
}
@InjectManager("baseRepository_")
async createPaymentSession(
paymentCollectionId: string,
input: CreatePaymentSessionDTO,
@MedusaContext() sharedContext?: Context
): Promise<PaymentSessionDTO> {
let paymentSession: PaymentSession
try {
const providerSessionSession =
await this.paymentProviderService_.createSession(input.provider_id, {
context: input.context ?? {},
amount: input.amount,
currency_code: input.currency_code,
})
input.data = {
...input.data,
...providerSessionSession,
}
paymentSession = await this.createPaymentSession_(
paymentCollectionId,
input,
sharedContext
)
} catch (error) {
// In case the session is created at the provider, but fails to be created in Medusa,
// we catch the error and delete the session at the provider and rethrow.
await this.paymentProviderService_.deleteSession({
provider_id: input.provider_id,
data: input.data,
})
throw error
}
return await this.baseRepository_.serialize(paymentSession, {
populate: true,
})
}
@InjectTransactionManager("baseRepository_")
async createPaymentSession_(
paymentCollectionId: string,
data: CreatePaymentSessionDTO,
@MedusaContext() sharedContext?: Context
): Promise<PaymentSession> {
const paymentSession = await this.paymentSessionService_.create(
{
payment_collection_id: paymentCollectionId,
provider_id: data.provider_id,
amount: data.amount,
currency_code: data.currency_code,
context: data.context,
data: data.data,
},
sharedContext
)
return paymentSession
}
@InjectTransactionManager("baseRepository_")
async updatePaymentSession(
data: UpdatePaymentSessionDTO,
@MedusaContext() sharedContext?: Context
): Promise<PaymentSessionDTO> {
const session = await this.paymentSessionService_.retrieve(
data.id,
{ select: ["id", "data", "provider_id"] },
sharedContext
)
const updated = await this.paymentSessionService_.update(
{
id: session.id,
amount: data.amount,
currency_code: data.currency_code,
data: data.data,
},
sharedContext
)
return await this.baseRepository_.serialize(updated[0], { populate: true })
}
@InjectTransactionManager("baseRepository_")
async deletePaymentSession(
id: string,
@MedusaContext() sharedContext?: Context
): Promise<void> {
const session = await this.paymentSessionService_.retrieve(
id,
{ select: ["id", "data", "provider_id"] },
sharedContext
)
await this.paymentProviderService_.deleteSession({
provider_id: session.provider_id,
data: session.data,
})
await this.paymentSessionService_.delete(id, sharedContext)
}
@InjectTransactionManager("baseRepository_")
async authorizePaymentSession(
id: string,
context: Record<string, unknown>,
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO> {
const session = await this.paymentSessionService_.retrieve(
id,
{
select: [
"id",
"data",
"provider_id",
"amount",
"currency_code",
"payment_collection_id",
],
},
sharedContext
)
// this method needs to be idempotent
if (session.authorized_at) {
const payment = await this.paymentService_.retrieve(
{ session_id: session.id },
{ relations: ["payment_collection"] },
sharedContext
)
return await this.baseRepository_.serialize(payment, { populate: true })
}
const { data, status } =
await this.paymentProviderService_.authorizePayment(
{
provider_id: session.provider_id,
data: session.data,
},
context
)
await this.paymentSessionService_.update(
{
id: session.id,
data,
status,
authorized_at:
status === PaymentSessionStatus.AUTHORIZED ? new Date() : null,
},
sharedContext
)
if (status !== PaymentSessionStatus.AUTHORIZED) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Session: ${session.id} is not authorized with the provider.`
)
}
// TODO: update status on payment collection if authorized_amount === amount - depends on the BigNumber PR
const payment = await this.paymentService_.create(
{
amount: session.amount,
currency_code: session.currency_code,
payment_session: session.id,
payment_collection_id: session.payment_collection_id,
provider_id: session.provider_id,
// customer_id: context.customer.id,
data,
},
sharedContext
)
return await this.retrievePayment(
payment.id,
{ relations: ["payment_collection"] },
sharedContext
)
}
@InjectTransactionManager("baseRepository_")
async updatePayment(
data: UpdatePaymentDTO,
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO> {
// NOTE: currently there is no update with the provider but maybe data could be updated
const result = await this.paymentService_.update(data, sharedContext)
return await this.baseRepository_.serialize<PaymentDTO>(result[0], {
populate: true,
})
}
@InjectTransactionManager("baseRepository_")
async capturePayment(
data: CreateCaptureDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<PaymentDTO> {
const payment = await this.paymentService_.retrieve(
data.payment_id,
{
select: [
"id",
"data",
"provider_id",
"amount",
"raw_amount",
"canceled_at",
],
relations: ["captures.raw_amount"],
},
sharedContext
)
// If no custom amount is passed, we assume the full amount needs to be captured
if (!data.amount) {
data.amount = payment.amount as number
}
if (payment.canceled_at) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The payment: ${payment.id} has been canceled.`
)
}
if (payment.captured_at) {
return await this.retrievePayment(
data.payment_id,
{ relations: ["captures"] },
sharedContext
)
}
const capturedAmount = payment.captures.reduce((captureAmount, next) => {
return MathBN.add(captureAmount, next.raw_amount)
}, MathBN.convert(0))
const authorizedAmount = new BigNumber(payment.raw_amount)
const newCaptureAmount = new BigNumber(data.amount)
const remainingToCapture = MathBN.sub(authorizedAmount, capturedAmount)
if (MathBN.gt(newCaptureAmount, remainingToCapture)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You cannot capture more than the authorized amount substracted by what is already captured.`
)
}
const paymentData = await this.paymentProviderService_.capturePayment({
data: payment.data!,
provider_id: payment.provider_id,
})
await this.captureService_.create(
{
payment: data.payment_id,
amount: data.amount,
captured_by: data.captured_by,
},
sharedContext
)
await this.paymentService_.update(
{ id: payment.id, data: paymentData },
sharedContext
)
// When the entire authorized amount has been captured, we mark it fully capture by setting the captured_at field
const totalCaptured = MathBN.convert(
MathBN.add(capturedAmount, newCaptureAmount)
)
if (MathBN.gte(totalCaptured, authorizedAmount)) {
await this.paymentService_.update(
{ id: payment.id, captured_at: new Date() },
sharedContext
)
}
return await this.retrievePayment(
payment.id,
{ relations: ["captures"] },
sharedContext
)
}
@InjectTransactionManager("baseRepository_")
async refundPayment(
data: CreateRefundDTO,
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO> {
const payment = await this.paymentService_.retrieve(
data.payment_id,
{
select: ["id", "data", "provider_id", "amount", "raw_amount"],
relations: ["captures.raw_amount"],
},
sharedContext
)
if (!data.amount) {
data.amount = payment.amount as number
}
const capturedAmount = payment.captures.reduce((captureAmount, next) => {
const amountAsBigNumber = new BigNumber(next.raw_amount)
return MathBN.add(captureAmount, amountAsBigNumber)
}, MathBN.convert(0))
const refundAmount = new BigNumber(data.amount)
if (MathBN.lt(capturedAmount, refundAmount)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You cannot refund more than what is captured on the payment.`
)
}
const paymentData = await this.paymentProviderService_.refundPayment(
{
data: payment.data!,
provider_id: payment.provider_id,
},
data.amount as number
)
await this.refundService_.create(
{
payment: data.payment_id,
amount: data.amount,
created_by: data.created_by,
},
sharedContext
)
await this.paymentService_.update(
{ id: payment.id, data: paymentData },
sharedContext
)
return await this.retrievePayment(
payment.id,
{ relations: ["refunds"] },
sharedContext
)
}
@InjectTransactionManager("baseRepository_")
async cancelPayment(
paymentId: string,
@MedusaContext() sharedContext?: Context
): Promise<PaymentDTO> {
const payment = await this.paymentService_.retrieve(
paymentId,
{ select: ["id", "data", "provider_id"] },
sharedContext
)
// TODO: revisit when totals are implemented
// if (payment.captured_amount !== 0) {
// throw new MedusaError(
// MedusaError.Types.INVALID_DATA,
// `Cannot cancel a payment: ${payment.id} that has been captured.`
// )
// }
await this.paymentProviderService_.cancelPayment({
data: payment.data!,
provider_id: payment.provider_id,
})
await this.paymentService_.update(
{ id: paymentId, canceled_at: new Date() },
sharedContext
)
return await this.retrievePayment(payment.id, {}, sharedContext)
}
@InjectTransactionManager("baseRepository_")
async processEvent(
eventData: ProviderWebhookPayload,
@MedusaContext() sharedContext?: Context
): Promise<void> {
const providerId = `pp_${eventData.provider}`
const event = await this.paymentProviderService_.getWebhookActionAndData(
providerId,
eventData.payload
)
if (event.action === PaymentActions.NOT_SUPPORTED) {
return
}
switch (event.action) {
case PaymentActions.SUCCESSFUL: {
const [payment] = await this.listPayments(
{
session_id: event.data.resource_id,
},
{},
sharedContext
)
await this.capturePayment(
{ payment_id: payment.id, amount: event.data.amount },
sharedContext
)
break
}
case PaymentActions.AUTHORIZED:
await this.authorizePaymentSession(
event.data.resource_id as string,
{},
sharedContext
)
}
}
@InjectManager("baseRepository_")
async listPaymentProviders(
filters: FilterablePaymentProviderProps = {},
config: FindConfig<PaymentProviderDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<PaymentProviderDTO[]> {
const providers = await this.paymentProviderService_.list(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<PaymentProviderDTO[]>(
providers,
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listAndCountPaymentProviders(
filters: FilterablePaymentProviderProps = {},
config: FindConfig<PaymentProviderDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<[PaymentProviderDTO[], number]> {
const [providers, count] = await this.paymentProviderService_.listAndCount(
filters,
config,
sharedContext
)
return [
await this.baseRepository_.serialize<PaymentProviderDTO[]>(providers, {
populate: true,
}),
count,
]
}
}

View File

@@ -0,0 +1,215 @@
import {
Context,
CreatePaymentProviderDTO,
CreatePaymentProviderSession,
DAL,
FilterablePaymentProviderProps,
FindConfig,
InternalModuleDeclaration,
IPaymentProvider,
PaymentProviderAuthorizeResponse,
PaymentProviderDataInput,
PaymentProviderDTO,
PaymentProviderError,
PaymentProviderSessionResponse,
PaymentSessionStatus,
ProviderWebhookPayload,
UpdatePaymentProviderSession,
WebhookActionResult,
} from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
isPaymentProviderError,
MedusaContext,
ModulesSdkUtils,
} from "@medusajs/utils"
import { PaymentProvider } from "@models"
import { MedusaError } from "medusa-core-utils"
import { EOL } from "os"
type InjectedDependencies = {
paymentProviderRepository: DAL.RepositoryService
[key: `pp_${string}`]: IPaymentProvider
}
export default class PaymentProviderService {
protected readonly container_: InjectedDependencies
protected readonly paymentProviderRepository_: DAL.RepositoryService
constructor(
container: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
this.container_ = container
this.paymentProviderRepository_ = container.paymentProviderRepository
}
@InjectTransactionManager("paymentProviderRepository_")
async create(
data: CreatePaymentProviderDTO[],
@MedusaContext() sharedContext?: Context
): Promise<PaymentProvider[]> {
return await this.paymentProviderRepository_.create(data, sharedContext)
}
@InjectManager("paymentProviderRepository_")
async list(
filters: FilterablePaymentProviderProps,
config: FindConfig<PaymentProviderDTO>,
@MedusaContext() sharedContext?: Context
): Promise<PaymentProvider[]> {
const queryOptions = ModulesSdkUtils.buildQuery<PaymentProvider>(
filters,
config
)
return await this.paymentProviderRepository_.find(
queryOptions,
sharedContext
)
}
@InjectManager("paymentProviderRepository_")
async listAndCount(
filters: FilterablePaymentProviderProps,
config: FindConfig<PaymentProviderDTO>,
@MedusaContext() sharedContext?: Context
): Promise<[PaymentProvider[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<PaymentProvider>(
filters,
config
)
return await this.paymentProviderRepository_.findAndCount(
queryOptions,
sharedContext
)
}
retrieveProvider(providerId: string): IPaymentProvider {
try {
return this.container_[providerId] as IPaymentProvider
} catch (e) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Could not find a payment provider with id: ${providerId}`
)
}
}
async createSession(
providerId: string,
sessionInput: CreatePaymentProviderSession
): Promise<PaymentProviderSessionResponse["data"]> {
const provider = this.retrieveProvider(providerId)
const paymentResponse = await provider.initiatePayment(sessionInput)
if (isPaymentProviderError(paymentResponse)) {
this.throwPaymentProviderError(paymentResponse)
}
return (paymentResponse as PaymentProviderSessionResponse).data
}
async updateSession(
providerId: string,
sessionInput: UpdatePaymentProviderSession
): Promise<Record<string, unknown> | undefined> {
const provider = this.retrieveProvider(providerId)
const paymentResponse = await provider.updatePayment(sessionInput)
if (isPaymentProviderError(paymentResponse)) {
this.throwPaymentProviderError(paymentResponse)
}
return (paymentResponse as PaymentProviderSessionResponse)?.data
}
async deleteSession(input: PaymentProviderDataInput): Promise<void> {
const provider = this.retrieveProvider(input.provider_id)
const error = await provider.deletePayment(input.data)
if (isPaymentProviderError(error)) {
this.throwPaymentProviderError(error)
}
}
async authorizePayment(
input: PaymentProviderDataInput,
context: Record<string, unknown>
): Promise<{ data: Record<string, unknown>; status: PaymentSessionStatus }> {
const provider = this.retrieveProvider(input.provider_id)
const res = await provider.authorizePayment(input.data, context)
if (isPaymentProviderError(res)) {
this.throwPaymentProviderError(res)
}
const { data, status } = res as PaymentProviderAuthorizeResponse
return { data, status }
}
async getStatus(
input: PaymentProviderDataInput
): Promise<PaymentSessionStatus> {
const provider = this.retrieveProvider(input.provider_id)
return await provider.getPaymentStatus(input.data)
}
async capturePayment(
input: PaymentProviderDataInput
): Promise<Record<string, unknown>> {
const provider = this.retrieveProvider(input.provider_id)
const res = await provider.capturePayment(input.data)
if (isPaymentProviderError(res)) {
this.throwPaymentProviderError(res)
}
return res as Record<string, unknown>
}
async cancelPayment(input: PaymentProviderDataInput): Promise<void> {
const provider = this.retrieveProvider(input.provider_id)
const error = await provider.cancelPayment(input.data)
if (isPaymentProviderError(error)) {
this.throwPaymentProviderError(error)
}
}
async refundPayment(
input: PaymentProviderDataInput,
amount: number
): Promise<Record<string, unknown>> {
const provider = this.retrieveProvider(input.provider_id)
const res = await provider.refundPayment(input.data, amount)
if (isPaymentProviderError(res)) {
this.throwPaymentProviderError(res)
}
return res as Record<string, unknown>
}
async getWebhookActionAndData(
providerId: string,
data: ProviderWebhookPayload["payload"]
): Promise<WebhookActionResult> {
const provider = this.retrieveProvider(providerId)
return await provider.getWebhookActionAndData(data)
}
private throwPaymentProviderError(errObj: PaymentProviderError) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`${errObj.error}${errObj.detail ? `:${EOL}${errObj.detail}` : ""}`,
errObj.code
)
}
}

View File

@@ -0,0 +1,27 @@
import {
Logger,
ModuleProviderExports,
ModuleServiceInitializeOptions,
} from "@medusajs/types"
export type InitializeModuleInjectableDependencies = {
logger?: Logger
}
export type PaymentModuleOptions = Partial<ModuleServiceInitializeOptions> & {
/**
* Providers to be registered
*/
providers?: {
/**
* The module provider to be registered
*/
resolve: string | ModuleProviderExports
options: {
/**
* key value pair of the provider name and the configuration to be passed to the provider constructor
*/
config: Record<string, unknown>
}
}[]
}