fix: Minor fixes and cleanup to the payments setup (#11356)

This PR adds a couple new statuses to the payment collection and payment webhook results. The payment collection will now be marked as "completed" once the captured amount is the full amount of the payment collection.

There are several things left to improve the payment setup, so non-happy-path cases are handled correctly.
1. Currently the payment session and payment models serve a very similar purpose. Part of the information is found on one, and the other part on the other model, without any clear reason for doing so. We can simplify the payment module and the data models simply by merging the two.
2. We need to handle failures more gracefully, such as setting the payment session status to failed when such a webhook comes in.
3. We should convert the payment collection status and the different amounts to calculated fields from the payment session, captures, and refunds, as they can easily be a source of inconsistencies.
This commit is contained in:
Stevche Radevski authored and GitHub committed 2025-02-09 15:42:02 +00:00
1 parent 3dbef519d9
commit 702d338284
17 files changed
+290 -208

No files matched your search

@@ -833,7 +833,7 @@ medusaIntegrationTestRunner({
expect.objectContaining({ expect.objectContaining({
currency_code: "usd", currency_code: "usd",
amount: paymentDelta, amount: paymentDelta,
status: "authorized", status: "completed",
authorized_amount: paymentDelta, authorized_amount: paymentDelta,
captured_amount: paymentDelta, captured_amount: paymentDelta,
refunded_amount: 0, refunded_amount: 0,
@@ -346,8 +346,7 @@ medusaIntegrationTestRunner({
expect(paymentCollection).toEqual( expect(paymentCollection).toEqual(
expect.objectContaining({ expect.objectContaining({
amount: 200, amount: 200,
// Q: Shouldn't this be paid? status: "completed",
status: "authorized",
payment_sessions: [ payment_sessions: [
expect.objectContaining({ expect.objectContaining({
status: "authorized", status: "authorized",
@@ -639,8 +638,7 @@ medusaIntegrationTestRunner({
expect(paymentCollection).toEqual( expect(paymentCollection).toEqual(
expect.objectContaining({ expect.objectContaining({
amount: 115.9, amount: 115.9,
// Q: Shouldn't this be paid? status: "completed",
status: "authorized",
payment_sessions: [ payment_sessions: [
expect.objectContaining({ expect.objectContaining({
status: "authorized", status: "authorized",
@@ -38,4 +38,6 @@ export const capturePaymentStep = createStep(
return new StepResponse(payment) return new StepResponse(payment)
} }
// We don't want to compensate a capture automatically as the actual funds have already been taken.
// The only want to compensate here is to issue a refund, but it's better to leave that as a manual operation for now.
) )
@@ -38,4 +38,6 @@ export const refundPaymentStep = createStep(
return new StepResponse(payment) return new StepResponse(payment)
} }
// We don't want to compensate a refund automatically as the actual funds have already been sent
// And in most cases we can't simply do another capture/authorization
) )
@@ -9,7 +9,8 @@ export type BasePaymentCollectionStatus =
| "authorized" | "authorized"
| "partially_authorized" | "partially_authorized"
| "canceled" | "canceled"
| "completed"
| "failed"
/** /**
* *
* The status of a payment session. * The status of a payment session.
@@ -10,6 +10,8 @@ export type PaymentCollectionStatus =
| "authorized" | "authorized"
| "partially_authorized" | "partially_authorized"
| "canceled" | "canceled"
| "failed"
| "completed"
export type PaymentSessionStatus = export type PaymentSessionStatus =
| "authorized" | "authorized"
@@ -56,6 +56,9 @@ export type PaymentActions =
| "authorized" | "authorized"
| "captured" | "captured"
| "failed" | "failed"
| "pending"
| "requires_more"
| "canceled"
| "not_supported" | "not_supported"
/** /**
@@ -1003,6 +1003,21 @@ export interface IPaymentModuleService extends IModuleService {
sharedContext?: Context sharedContext?: Context
): Promise<CaptureDTO[]> ): Promise<CaptureDTO[]>
/**
* This method deletes a capture by its ID.
*
* @param {string[]} captureId - The capture's ID.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<void>} Resolves when the capture is deleted successfully.
*
* @example
* await paymentModuleService.deleteCaptures([
* "capt_123",
* "capt_321",
* ])
*/
deleteCaptures(ids: string[], sharedContext?: Context): Promise<void>
/** /**
* This method retrieves a paginated list of refunds based on optional filters and configuration. * This method retrieves a paginated list of refunds based on optional filters and configuration.
* *
@@ -1055,6 +1070,21 @@ export interface IPaymentModuleService extends IModuleService {
sharedContext?: Context sharedContext?: Context
): Promise<RefundDTO[]> ): Promise<RefundDTO[]>
/**
* This method deletes a refund by its ID.
*
* @param {string[]} refundId - The refund's ID.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<void>} Resolves when the refund is deleted successfully.
*
* @example
* await paymentModuleService.deleteRefunds([
* "ref_123",
* "ref_321",
* ])
*/
deleteRefunds(ids: string[], sharedContext?: Context): Promise<void>
/** /**
* This method creates refund reasons. * This method creates refund reasons.
* *
@@ -24,4 +24,12 @@ export enum PaymentCollectionStatus {
* The payment collection is canceled. * The payment collection is canceled.
*/ */
CANCELED = "canceled", CANCELED = "canceled",
/**
* The payment collection is failed.
*/
FAILED = "failed",
/**
* The payment collection is completed.
*/
COMPLETED = "completed",
} }
+13 -1
View File
@@ -3,7 +3,7 @@ export enum PaymentWebhookEvents {
} }
/** /**
* Normalized events from payment provider to internal payment module events. * Normalized events from payment provider to internal payment module events. In principle, these should match the payment status.
*/ */
export enum PaymentActions { export enum PaymentActions {
/** /**
@@ -18,6 +18,18 @@ export enum PaymentActions {
* Payment failed. * Payment failed.
*/ */
FAILED = "failed", FAILED = "failed",
/**
* Payment is pending.
*/
PENDING = "pending",
/**
* Payment requires more information.
*/
REQUIRES_MORE = "requires_more",
/**
* Payment was canceled.
*/
CANCELED = "canceled",
/** /**
* Received an event that is not processable. * Received an event that is not processable.
*/ */
@@ -35,7 +35,14 @@ export default async function paymentWebhookhandler({
const processedEvent = await paymentService.getWebhookActionAndData(input) const processedEvent = await paymentService.getWebhookActionAndData(input)
if (processedEvent?.action === PaymentActions.NOT_SUPPORTED) { if (
processedEvent?.action === PaymentActions.NOT_SUPPORTED ||
// Currently none of these are handled by the processPaymentWorkflow, so we ignore them.
// Remove once the processPaymentWorkflow is handling them.
processedEvent?.action === PaymentActions.CANCELED ||
processedEvent?.action === PaymentActions.FAILED ||
processedEvent?.action === PaymentActions.REQUIRES_MORE
) {
return return
} }
@@ -139,7 +139,7 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
amount: 200, amount: 200,
authorized_amount: 200, authorized_amount: 200,
captured_amount: 200, captured_amount: 200,
status: "authorized", status: "completed",
deleted_at: null, deleted_at: null,
completed_at: expect.any(Date), completed_at: expect.any(Date),
payment_sessions: [ payment_sessions: [
@@ -209,7 +209,9 @@
"awaiting", "awaiting",
"authorized", "authorized",
"partially_authorized", "partially_authorized",
"canceled" "canceled",
"failed",
"completed"
], ],
"mappedType": "enum" "mappedType": "enum"
}, },
@@ -0,0 +1,17 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20250207132723 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table if exists "payment_collection" drop constraint if exists "payment_collection_status_check";`);
this.addSql(`alter table if exists "payment_collection" add constraint "payment_collection_status_check" check("status" in ('not_paid', 'awaiting', 'authorized', 'partially_authorized', 'canceled', 'failed', 'completed'));`);
}
override async down(): Promise<void> {
this.addSql(`alter table if exists "payment_collection" drop constraint if exists "payment_collection_status_check";`);
this.addSql(`alter table if exists "payment_collection" add constraint "payment_collection_status_check" check("status" in ('not_paid', 'awaiting', 'authorized', 'partially_authorized', 'canceled'));`);
}
}
@@ -4,6 +4,8 @@ import PaymentCollection from "./payment-collection"
import PaymentSession from "./payment-session" import PaymentSession from "./payment-session"
import Refund from "./refund" import Refund from "./refund"
// TODO: We should remove the `Payment` model and use the `PaymentSession` model instead.
// We just need to move the refunds, captures, canceled_at, and captured_at to it.
const Payment = model const Payment = model
.define("Payment", { .define("Payment", {
id: model.id({ prefix: "pay" }).primaryKey(), id: model.id({ prefix: "pay" }).primaryKey(),
@@ -12,7 +12,6 @@ import {
FilterablePaymentCollectionProps, FilterablePaymentCollectionProps,
FilterablePaymentMethodProps, FilterablePaymentMethodProps,
FilterablePaymentProviderProps, FilterablePaymentProviderProps,
FilterablePaymentSessionProps,
FindConfig, FindConfig,
InferEntityType, InferEntityType,
InternalModuleDeclaration, InternalModuleDeclaration,
@@ -303,6 +302,7 @@ export default class PaymentModuleService
sharedContext?: Context sharedContext?: Context
): Promise<PaymentCollectionDTO[]> ): Promise<PaymentCollectionDTO[]>
// Should we remove this and use `updatePaymentCollections` instead?
@InjectManager() @InjectManager()
async completePaymentCollections( async completePaymentCollections(
paymentCollectionId: string | string[], paymentCollectionId: string | string[],
@@ -415,6 +415,12 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
await this.paymentProviderService_.updateSession(session.provider_id, {
data: data.data,
amount: data.amount,
currency_code: data.currency_code,
})
const updated = await this.paymentSessionService_.update( const updated = await this.paymentSessionService_.update(
{ {
id: session.id, id: session.id,
@@ -491,7 +497,7 @@ export default class PaymentModuleService
) { ) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_ALLOWED, MedusaError.Types.NOT_ALLOWED,
`Session: ${session.id} is not authorized with the provider.` `Session: ${session.id} was not authorized with the provider.`
) )
} }
@@ -516,11 +522,9 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
return await this.retrievePayment( return await this.baseRepository_.serialize(payment, {
payment.id, populate: true,
{ relations: ["payment_collection"] }, })
sharedContext
)
} }
@InjectTransactionManager() @InjectTransactionManager()
@@ -541,8 +545,11 @@ export default class PaymentModuleService
id: session.id, id: session.id,
data, data,
status, status,
authorized_at: ...(session.authorized_at === null
status === PaymentSessionStatus.AUTHORIZED ? new Date() : null, ? {
authorized_at: new Date(),
}
: {}),
}, },
sharedContext sharedContext
) )
@@ -569,38 +576,6 @@ export default class PaymentModuleService
return payment return payment
} }
@InjectManager()
// @ts-expect-error
async retrievePaymentSession(
id: string,
config: FindConfig<PaymentSessionDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<PaymentSessionDTO> {
const session = await this.paymentSessionService_.retrieve(
id,
config,
sharedContext
)
return await this.baseRepository_.serialize(session)
}
@InjectManager()
// @ts-expect-error
async listPaymentSessions(
filters?: FilterablePaymentSessionProps,
config?: FindConfig<PaymentSessionDTO>,
sharedContext?: Context
): Promise<PaymentSessionDTO[]> {
const sessions = await this.paymentSessionService_.list(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<PaymentSessionDTO[]>(sessions)
}
@InjectManager() @InjectManager()
async updatePayment( async updatePayment(
data: UpdatePaymentDTO, data: UpdatePaymentDTO,
@@ -612,13 +587,33 @@ export default class PaymentModuleService
return await this.baseRepository_.serialize<PaymentDTO>(result[0]) return await this.baseRepository_.serialize<PaymentDTO>(result[0])
} }
// TODO: This method should return a capture, not a payment
@InjectManager() @InjectManager()
async capturePayment( async capturePayment(
data: CreateCaptureDTO, data: CreateCaptureDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<PaymentDTO> { ): Promise<PaymentDTO> {
const { payment, isFullyCaptured, capture } = await this.capturePayment_( const payment = await this.paymentService_.retrieve(
data.payment_id,
{
select: [
"id",
"data",
"provider_id",
"payment_collection_id",
"amount",
"raw_amount",
"captured_at",
"canceled_at",
],
relations: ["captures.raw_amount"],
},
sharedContext
)
const { isFullyCaptured, capture } = await this.capturePayment_(
data, data,
payment,
sharedContext sharedContext
) )
@@ -640,45 +635,20 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
return await this.retrievePayment( return await this.baseRepository_.serialize(payment, {
payment.id, populate: true,
{ relations: ["captures"] }, })
sharedContext
)
} }
@InjectTransactionManager() @InjectTransactionManager()
private async capturePayment_( private async capturePayment_(
data: CreateCaptureDTO, data: CreateCaptureDTO,
payment: InferEntityType<typeof Payment>,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<{ ): Promise<{
payment: InferEntityType<typeof Payment>
isFullyCaptured: boolean isFullyCaptured: boolean
capture?: InferEntityType<typeof Capture> capture?: InferEntityType<typeof Capture>
}> { }> {
const payment = await this.paymentService_.retrieve(
data.payment_id,
{
select: [
"id",
"data",
"provider_id",
"payment_collection_id",
"amount",
"raw_amount",
"captured_at",
"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) { if (payment.canceled_at) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
@@ -687,7 +657,12 @@ export default class PaymentModuleService
} }
if (payment.captured_at) { if (payment.captured_at) {
return { payment, isFullyCaptured: true } return { isFullyCaptured: true }
}
// If no custom amount is passed, we assume the full amount needs to be captured
if (!data.amount) {
data.amount = payment.amount as number
} }
const capturedAmount = payment.captures.reduce((captureAmount, next) => { const capturedAmount = payment.captures.reduce((captureAmount, next) => {
@@ -720,7 +695,7 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
return { payment, isFullyCaptured, capture } return { isFullyCaptured, capture }
} }
@InjectManager() @InjectManager()
private async capturePaymentFromProvider_( private async capturePaymentFromProvider_(
@@ -875,15 +850,79 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
async getWebhookActionAndData( private async maybeUpdatePaymentCollection_(
eventData: ProviderWebhookPayload, paymentCollectionId: string,
@MedusaContext() sharedContext?: Context sharedContext?: Context
): Promise<WebhookActionResult> { ) {
const providerId = `pp_${eventData.provider}` const paymentCollection = await this.paymentCollectionService_.retrieve(
paymentCollectionId,
{
select: ["amount", "raw_amount", "status"],
relations: [
"payment_sessions.amount",
"payment_sessions.raw_amount",
"payments.captures.amount",
"payments.captures.raw_amount",
"payments.refunds.amount",
"payments.refunds.raw_amount",
],
},
sharedContext
)
return await this.paymentProviderService_.getWebhookActionAndData( const paymentSessions = paymentCollection.payment_sessions
providerId, const captures = paymentCollection.payments
eventData.payload .map((pay) => [...pay.captures])
.flat()
const refunds = paymentCollection.payments
.map((pay) => [...pay.refunds])
.flat()
let authorizedAmount = MathBN.convert(0)
let capturedAmount = MathBN.convert(0)
let refundedAmount = MathBN.convert(0)
let completedAt: Date | undefined
for (const ps of paymentSessions) {
if (ps.status === PaymentSessionStatus.AUTHORIZED) {
authorizedAmount = MathBN.add(authorizedAmount, ps.amount)
}
}
for (const capture of captures) {
capturedAmount = MathBN.add(capturedAmount, capture.amount)
}
for (const refund of refunds) {
refundedAmount = MathBN.add(refundedAmount, refund.amount)
}
let status =
paymentSessions.length === 0
? PaymentCollectionStatus.NOT_PAID
: PaymentCollectionStatus.AWAITING
if (MathBN.gt(authorizedAmount, 0)) {
status = MathBN.gte(authorizedAmount, paymentCollection.amount)
? PaymentCollectionStatus.AUTHORIZED
: PaymentCollectionStatus.PARTIALLY_AUTHORIZED
}
if (MathBN.eq(paymentCollection.amount, capturedAmount)) {
status = PaymentCollectionStatus.COMPLETED
completedAt = new Date()
}
await this.paymentCollectionService_.update(
{
id: paymentCollectionId,
status,
authorized_amount: authorizedAmount,
captured_amount: capturedAmount,
refunded_amount: refundedAmount,
completed_at: completedAt,
},
sharedContext
) )
} }
@@ -939,44 +978,23 @@ export default class PaymentModuleService
let accountHolder: InferEntityType<typeof AccountHolder> | undefined let accountHolder: InferEntityType<typeof AccountHolder> | undefined
let providerAccountHolder: CreateAccountHolderOutput | undefined let providerAccountHolder: CreateAccountHolderOutput | undefined
try { providerAccountHolder =
providerAccountHolder = await this.paymentProviderService_.createAccountHolder(
await this.paymentProviderService_.createAccountHolder( input.provider_id,
input.provider_id, { context: input.context }
{ context: input.context } )
)
// This can be empty when either the method is not supported or an account holder wasn't created // This can be empty when either the method is not supported or an account holder wasn't created
if (isPresent(providerAccountHolder)) { if (isPresent(providerAccountHolder)) {
accountHolder = await this.accountHolderService_.create( accountHolder = await this.accountHolderService_.create(
{ {
external_id: providerAccountHolder.id, external_id: providerAccountHolder.id,
email: input.context.customer?.email, email: input.context.customer?.email,
data: providerAccountHolder.data, data: providerAccountHolder.data,
provider_id: input.provider_id, provider_id: input.provider_id,
}, },
sharedContext sharedContext
) )
}
} catch (error) {
if (providerAccountHolder) {
await this.paymentProviderService_.deleteAccountHolder(
input.provider_id,
{
context: {
account_holder: providerAccountHolder as {
data: Record<string, unknown>
},
},
}
)
}
if (accountHolder) {
await this.accountHolderService_.delete(accountHolder.id, sharedContext)
}
throw error
} }
return await this.baseRepository_.serialize(accountHolder) return await this.baseRepository_.serialize(accountHolder)
@@ -1078,72 +1096,15 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
private async maybeUpdatePaymentCollection_( async getWebhookActionAndData(
paymentCollectionId: string, eventData: ProviderWebhookPayload,
sharedContext?: Context @MedusaContext() sharedContext?: Context
) { ): Promise<WebhookActionResult> {
const paymentCollection = await this.paymentCollectionService_.retrieve( const providerId = `pp_${eventData.provider}`
paymentCollectionId,
{
select: ["amount", "raw_amount", "status"],
relations: [
"payment_sessions.amount",
"payment_sessions.raw_amount",
"payments.captures.amount",
"payments.captures.raw_amount",
"payments.refunds.amount",
"payments.refunds.raw_amount",
],
},
sharedContext
)
const paymentSessions = paymentCollection.payment_sessions return await this.paymentProviderService_.getWebhookActionAndData(
const captures = paymentCollection.payments providerId,
.map((pay) => [...pay.captures]) eventData.payload
.flat()
const refunds = paymentCollection.payments
.map((pay) => [...pay.refunds])
.flat()
let authorizedAmount = MathBN.convert(0)
let capturedAmount = MathBN.convert(0)
let refundedAmount = MathBN.convert(0)
for (const ps of paymentSessions) {
if (ps.status === PaymentSessionStatus.AUTHORIZED) {
authorizedAmount = MathBN.add(authorizedAmount, ps.amount)
}
}
for (const capture of captures) {
capturedAmount = MathBN.add(capturedAmount, capture.amount)
}
for (const refund of refunds) {
refundedAmount = MathBN.add(refundedAmount, refund.amount)
}
let status =
paymentSessions.length === 0
? PaymentCollectionStatus.NOT_PAID
: PaymentCollectionStatus.AWAITING
if (MathBN.gt(authorizedAmount, 0)) {
status = MathBN.gte(authorizedAmount, paymentCollection.amount)
? PaymentCollectionStatus.AUTHORIZED
: PaymentCollectionStatus.PARTIALLY_AUTHORIZED
}
await this.paymentCollectionService_.update(
{
id: paymentCollectionId,
status,
authorized_amount: authorizedAmount,
captured_amount: capturedAmount,
refunded_amount: refundedAmount,
},
sharedContext
) )
} }
} }
@@ -125,22 +125,30 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
} }
const paymentIntent = await this.stripe_.paymentIntents.retrieve(id) const paymentIntent = await this.stripe_.paymentIntents.retrieve(id)
const dataResponse = paymentIntent as unknown as Record<string, unknown>
switch (paymentIntent.status) { switch (paymentIntent.status) {
case "requires_payment_method": case "requires_payment_method":
if (paymentIntent.last_payment_error) {
return { status: PaymentSessionStatus.ERROR, data: dataResponse }
}
return { status: PaymentSessionStatus.PENDING, data: dataResponse }
case "requires_confirmation": case "requires_confirmation":
case "processing": case "processing":
return { status: PaymentSessionStatus.PENDING } return { status: PaymentSessionStatus.PENDING, data: dataResponse }
case "requires_action": case "requires_action":
return { status: PaymentSessionStatus.REQUIRES_MORE } return {
status: PaymentSessionStatus.REQUIRES_MORE,
data: dataResponse,
}
case "canceled": case "canceled":
return { status: PaymentSessionStatus.CANCELED } return { status: PaymentSessionStatus.CANCELED, data: dataResponse }
case "requires_capture": case "requires_capture":
return { status: PaymentSessionStatus.AUTHORIZED } return { status: PaymentSessionStatus.AUTHORIZED, data: dataResponse }
case "succeeded": case "succeeded":
return { status: PaymentSessionStatus.CAPTURED } return { status: PaymentSessionStatus.CAPTURED, data: dataResponse }
default: default:
return { status: PaymentSessionStatus.PENDING } return { status: PaymentSessionStatus.PENDING, data: dataResponse }
} }
} }
@@ -419,24 +427,23 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
const intent = event.data.object as Stripe.PaymentIntent const intent = event.data.object as Stripe.PaymentIntent
const { currency } = intent const { currency } = intent
switch (event.type) { switch (event.type) {
case "payment_intent.amount_capturable_updated": case "payment_intent.created":
case "payment_intent.processing":
return { return {
action: PaymentActions.AUTHORIZED, action: PaymentActions.PENDING,
data: { data: {
session_id: intent.metadata.session_id, session_id: intent.metadata.session_id,
amount: getAmountFromSmallestUnit( amount: getAmountFromSmallestUnit(intent.amount, currency),
intent.amount_capturable,
currency
), // NOTE: revisit when implementing multicapture
}, },
} }
case "payment_intent.succeeded": case "payment_intent.canceled":
return { return {
action: PaymentActions.SUCCESSFUL, action: PaymentActions.CANCELED,
data: { data: {
session_id: intent.metadata.session_id, session_id: intent.metadata.session_id,
amount: getAmountFromSmallestUnit(intent.amount_received, currency), amount: getAmountFromSmallestUnit(intent.amount, currency),
}, },
} }
case "payment_intent.payment_failed": case "payment_intent.payment_failed":
@@ -447,6 +454,34 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
amount: getAmountFromSmallestUnit(intent.amount, currency), amount: getAmountFromSmallestUnit(intent.amount, currency),
}, },
} }
case "payment_intent.requires_action":
return {
action: PaymentActions.REQUIRES_MORE,
data: {
session_id: intent.metadata.session_id,
amount: getAmountFromSmallestUnit(intent.amount, currency),
},
}
case "payment_intent.amount_capturable_updated":
return {
action: PaymentActions.AUTHORIZED,
data: {
session_id: intent.metadata.session_id,
amount: getAmountFromSmallestUnit(
intent.amount_capturable,
currency
),
},
}
case "payment_intent.succeeded":
return {
action: PaymentActions.SUCCESSFUL,
data: {
session_id: intent.metadata.session_id,
amount: getAmountFromSmallestUnit(intent.amount_received, currency),
},
}
default: default:
return { action: PaymentActions.NOT_SUPPORTED } return { action: PaymentActions.NOT_SUPPORTED }
} }