feat: Create payment sessions (#6549)
~~Opening a draft PR to discuss a couple of implementation details that we should align on~~ **What** Add workflow and API endpoint for creating payment sessions for a payment collection. Endpoint is currently `POST /store/payment-collection/:id/payment-sessions`. I suggested an alternative in a comment below. Please note, we intentionally do not want to support creating payment sessions in bulk, as this would become a mess when having to manage multiple calls to third-party providers.
This commit is contained in:
1 parent
908b1dc3a2
commit
84208aafc1
30 files changed
+603
-182
No files matched your search
@@ -22,6 +22,8 @@ export class Migration20240225134525 extends Migration {
|
||||
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;
|
||||
@@ -31,6 +33,7 @@ export class Migration20240225134525 extends Migration {
|
||||
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 "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;
|
||||
@@ -176,7 +179,7 @@ export class Migration20240225134525 extends Migration {
|
||||
"captured_at" TIMESTAMPTZ NULL,
|
||||
"canceled_at" TIMESTAMPTZ NULL,
|
||||
"payment_collection_id" TEXT NOT NULL,
|
||||
"session_id" TEXT NOT NULL,
|
||||
"payment_session_id" TEXT NOT NULL,
|
||||
"metadata" JSONB NULL,
|
||||
CONSTRAINT "payment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
@@ -84,12 +84,12 @@ export default class PaymentCollection {
|
||||
payment_providers = new Collection<PaymentProvider>(this)
|
||||
|
||||
@OneToMany(() => PaymentSession, (ps) => ps.payment_collection, {
|
||||
cascade: [Cascade.REMOVE],
|
||||
cascade: [Cascade.PERSIST, "soft-remove"] as any,
|
||||
})
|
||||
payment_sessions = new Collection<PaymentSession>(this)
|
||||
|
||||
@OneToMany(() => Payment, (payment) => payment.payment_collection, {
|
||||
cascade: [Cascade.REMOVE],
|
||||
cascade: [Cascade.PERSIST, "soft-remove"] as any,
|
||||
})
|
||||
payments = new Collection<Payment>(this)
|
||||
|
||||
|
||||
@@ -54,19 +54,24 @@ export default class PaymentSession {
|
||||
})
|
||||
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",
|
||||
onDelete: "cascade",
|
||||
mapToPk: true,
|
||||
})
|
||||
payment_collection!: PaymentCollection
|
||||
payment_collection_id: string
|
||||
|
||||
@OneToOne({
|
||||
entity: () => Payment,
|
||||
mappedBy: (payment) => payment.payment_session,
|
||||
cascade: ["soft-remove"] as any,
|
||||
nullable: true,
|
||||
mappedBy: "payment_session",
|
||||
})
|
||||
payment?: Payment | null
|
||||
|
||||
|
||||
@@ -109,18 +109,26 @@ export default class Payment {
|
||||
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",
|
||||
onDelete: "cascade",
|
||||
mapToPk: true,
|
||||
})
|
||||
payment_collection!: PaymentCollection
|
||||
payment_collection_id: string
|
||||
|
||||
@OneToOne({
|
||||
owner: true,
|
||||
fieldName: "session_id",
|
||||
fieldName: "payment_session_id",
|
||||
index: "IDX_payment_payment_session_id",
|
||||
})
|
||||
payment_session!: PaymentSession
|
||||
payment_session: PaymentSession
|
||||
|
||||
/** COMPUTED PROPERTIES START **/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
PaymentProviderContext,
|
||||
CreatePaymentProviderSession,
|
||||
PaymentProviderError,
|
||||
PaymentProviderSessionResponse,
|
||||
PaymentSessionStatus,
|
||||
@@ -21,7 +21,7 @@ export class SystemProviderService extends AbstractPaymentProvider {
|
||||
}
|
||||
|
||||
async initiatePayment(
|
||||
context: PaymentProviderContext
|
||||
context: CreatePaymentProviderSession
|
||||
): Promise<PaymentProviderSessionResponse> {
|
||||
return { data: {} }
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
UpdatePaymentSessionDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
@@ -35,7 +36,6 @@ import {
|
||||
PaymentSession,
|
||||
Refund,
|
||||
} from "@models"
|
||||
|
||||
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
||||
import PaymentProviderService from "./payment-provider"
|
||||
|
||||
@@ -49,7 +49,7 @@ type InjectedDependencies = {
|
||||
paymentProviderService: PaymentProviderService
|
||||
}
|
||||
|
||||
const generateMethodForModels = [PaymentCollection, Payment]
|
||||
const generateMethodForModels = [PaymentCollection, Payment, PaymentSession]
|
||||
|
||||
export default class PaymentModuleService<
|
||||
TPaymentCollection extends PaymentCollection = PaymentCollection,
|
||||
@@ -201,44 +201,67 @@ export default class PaymentModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
@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<PaymentSessionDTO> {
|
||||
const created = await this.paymentSessionService_.create(
|
||||
): Promise<PaymentSession> {
|
||||
const paymentSession = await this.paymentSessionService_.create(
|
||||
{
|
||||
payment_collection_id: paymentCollectionId,
|
||||
provider_id: data.provider_id,
|
||||
amount: data.providerContext.amount,
|
||||
currency_code: data.providerContext.currency_code,
|
||||
payment_collection: paymentCollectionId,
|
||||
amount: data.amount,
|
||||
currency_code: data.currency_code,
|
||||
context: data.context,
|
||||
data: data.data,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
try {
|
||||
const sessionData = await this.paymentProviderService_.createSession(
|
||||
data.provider_id,
|
||||
{
|
||||
...data.providerContext,
|
||||
resource_id: created.id,
|
||||
}
|
||||
)
|
||||
|
||||
await this.paymentSessionService_.update(
|
||||
{
|
||||
id: created.id,
|
||||
data: sessionData,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return await this.baseRepository_.serialize(created, { populate: true })
|
||||
} catch (e) {
|
||||
await this.paymentSessionService_.delete([created.id], sharedContext)
|
||||
throw e
|
||||
}
|
||||
return paymentSession
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
@@ -252,17 +275,12 @@ export default class PaymentModuleService<
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const sessionData = await this.paymentProviderService_.updateSession(
|
||||
session.provider_id,
|
||||
data.providerContext
|
||||
)
|
||||
|
||||
const updated = await this.paymentSessionService_.update(
|
||||
{
|
||||
id: session.id,
|
||||
amount: data.providerContext.amount,
|
||||
currency_code: data.providerContext.currency_code,
|
||||
data: sessionData,
|
||||
amount: data.amount,
|
||||
currency_code: data.currency_code,
|
||||
data: data.data,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
@@ -298,8 +316,14 @@ export default class PaymentModuleService<
|
||||
const session = await this.paymentSessionService_.retrieve(
|
||||
id,
|
||||
{
|
||||
select: ["id", "data", "provider_id", "amount", "currency_code"],
|
||||
relations: ["payment_collection"],
|
||||
select: [
|
||||
"id",
|
||||
"data",
|
||||
"provider_id",
|
||||
"amount",
|
||||
"currency_code",
|
||||
"payment_collection_id",
|
||||
],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
@@ -348,7 +372,7 @@ export default class PaymentModuleService<
|
||||
amount: session.amount,
|
||||
currency_code: session.currency_code,
|
||||
payment_session: session.id,
|
||||
payment_collection: session.payment_collection!.id,
|
||||
payment_collection_id: session.payment_collection_id,
|
||||
provider_id: session.provider_id,
|
||||
// customer_id: context.customer.id,
|
||||
data,
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { EOL } from "os"
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import {
|
||||
Context,
|
||||
CreatePaymentProviderDTO,
|
||||
CreatePaymentProviderSession,
|
||||
DAL,
|
||||
InternalModuleDeclaration,
|
||||
IPaymentProvider,
|
||||
PaymentProviderAuthorizeResponse,
|
||||
PaymentProviderContext,
|
||||
PaymentProviderDataInput,
|
||||
PaymentProviderError,
|
||||
PaymentProviderSessionResponse,
|
||||
PaymentSessionStatus,
|
||||
ProviderWebhookPayload,
|
||||
UpdatePaymentProviderSession,
|
||||
WebhookActionResult,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
@@ -21,8 +20,9 @@ import {
|
||||
isPaymentProviderError,
|
||||
MedusaContext,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { PaymentProvider } from "@models"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EOL } from "os"
|
||||
|
||||
type InjectedDependencies = {
|
||||
paymentProviderRepository: DAL.RepositoryService
|
||||
@@ -70,20 +70,10 @@ export default class PaymentProviderService {
|
||||
|
||||
async createSession(
|
||||
providerId: string,
|
||||
sessionInput: PaymentProviderContext
|
||||
sessionInput: CreatePaymentProviderSession
|
||||
): Promise<PaymentProviderSessionResponse["data"]> {
|
||||
const provider = this.retrieveProvider(providerId)
|
||||
|
||||
if (
|
||||
!isDefined(sessionInput.currency_code) ||
|
||||
!isDefined(sessionInput.amount)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"`currency_code` and `amount` are required to create payment session."
|
||||
)
|
||||
}
|
||||
|
||||
const paymentResponse = await provider.initiatePayment(sessionInput)
|
||||
|
||||
if (isPaymentProviderError(paymentResponse)) {
|
||||
@@ -95,7 +85,7 @@ export default class PaymentProviderService {
|
||||
|
||||
async updateSession(
|
||||
providerId: string,
|
||||
sessionInput: PaymentProviderContext
|
||||
sessionInput: UpdatePaymentProviderSession
|
||||
): Promise<Record<string, unknown> | undefined> {
|
||||
const provider = this.retrieveProvider(providerId)
|
||||
|
||||
|
||||
Reference in new issue
Block a user