fix(payment): round curency precision (#12803)

This commit is contained in:
Carlos R. L. Rodrigues
2025-06-24 13:41:14 -03:00
committed by GitHub
parent 93cf79cb0f
commit ba1e6595b7
4 changed files with 151 additions and 17 deletions

View File

@@ -168,6 +168,79 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
})
)
})
it("complete payment flow successfully when rounded numbers are equal", async () => {
let paymentCollection = await service.createPaymentCollections({
currency_code: "usd",
amount: 200.129,
})
const paymentSession = await service.createPaymentSession(
paymentCollection.id,
{
provider_id: "pp_system_default",
amount: 200.129,
currency_code: "usd",
data: {},
context: {
customer: { id: "cus-id-1", email: "new@test.tsst" },
},
}
)
const payment = await service.authorizePaymentSession(
paymentSession.id,
{}
)
await service.capturePayment({
amount: 200.13, // rounded from payment provider
payment_id: payment.id,
})
await service.completePaymentCollections(paymentCollection.id)
paymentCollection = await service.retrievePaymentCollection(
paymentCollection.id,
{ relations: ["payment_sessions", "payments.captures"] }
)
expect(paymentCollection).toEqual(
expect.objectContaining({
id: expect.any(String),
currency_code: "usd",
amount: 200.129,
authorized_amount: 200.129,
captured_amount: 200.13,
status: "completed",
deleted_at: null,
completed_at: expect.any(Date),
payment_sessions: [
expect.objectContaining({
id: expect.any(String),
currency_code: "usd",
amount: 200.129,
provider_id: "pp_system_default",
status: "authorized",
authorized_at: expect.any(Date),
}),
],
payments: [
expect.objectContaining({
id: expect.any(String),
amount: 200.129,
currency_code: "usd",
provider_id: "pp_system_default",
captures: [
expect.objectContaining({
amount: 200.13,
}),
],
}),
],
})
)
})
})
describe("PaymentCollection", () => {
@@ -1032,7 +1105,7 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
expect(finalCollection).toEqual(
expect.objectContaining({
status: "authorized",
status: "completed",
amount: 500,
authorized_amount: 1000,
captured_amount: 1000,

View File

@@ -1,19 +1,22 @@
import {
AccountHolderDTO,
BigNumberInput,
CaptureDTO,
Context,
CreateAccountHolderDTO,
CreateAccountHolderOutput,
CreateCaptureDTO,
CreatePaymentCollectionDTO,
CreatePaymentMethodDTO,
CreatePaymentSessionDTO,
CreateRefundDTO,
AccountHolderDTO,
DAL,
FilterablePaymentCollectionProps,
FilterablePaymentMethodProps,
FilterablePaymentProviderProps,
FindConfig,
InferEntityType,
InitiatePaymentOutput,
InternalModuleDeclaration,
IPaymentModuleService,
Logger,
@@ -28,16 +31,13 @@ import {
ProviderWebhookPayload,
RefundDTO,
RefundReasonDTO,
UpdateAccountHolderDTO,
UpdateAccountHolderOutput,
UpdatePaymentCollectionDTO,
UpdatePaymentDTO,
UpdatePaymentSessionDTO,
CreateAccountHolderDTO,
UpsertPaymentCollectionDTO,
WebhookActionResult,
CreateAccountHolderOutput,
InitiatePaymentOutput,
UpdateAccountHolderDTO,
UpdateAccountHolderOutput,
} from "@medusajs/framework/types"
import {
BigNumber,
@@ -152,6 +152,25 @@ export default class PaymentModuleService
return joinerConfig
}
protected roundToCurrencyPrecision(
amount: BigNumberInput,
currencyCode: string
): BigNumberInput {
let precision: number | undefined = undefined
try {
const formatted = Intl.NumberFormat(undefined, {
style: "currency",
currency: currencyCode,
}).format(0.1111111)
precision = formatted.split(".")[1].length
} catch {
// Unknown currency, keep the full precision
}
return MathBN.convert(amount, precision)
}
// @ts-expect-error
createPaymentCollections(
data: CreatePaymentCollectionDTO,
@@ -627,6 +646,7 @@ export default class PaymentModuleService
"payment_collection_id",
"amount",
"raw_amount",
"currency_code",
"captured_at",
"canceled_at",
],
@@ -698,7 +718,12 @@ export default class PaymentModuleService
const newCaptureAmount = new BigNumber(data.amount)
const remainingToCapture = MathBN.sub(authorizedAmount, capturedAmount)
if (MathBN.gt(newCaptureAmount, remainingToCapture)) {
if (
MathBN.gt(
this.roundToCurrencyPrecision(newCaptureAmount, payment.currency_code),
this.roundToCurrencyPrecision(remainingToCapture, payment.currency_code)
)
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You cannot capture more than the authorized amount substracted by what is already captured.`
@@ -709,7 +734,10 @@ export default class PaymentModuleService
const totalCaptured = MathBN.convert(
MathBN.add(capturedAmount, newCaptureAmount)
)
const isFullyCaptured = MathBN.gte(totalCaptured, authorizedAmount)
const isFullyCaptured = MathBN.gte(
this.roundToCurrencyPrecision(totalCaptured, payment.currency_code),
this.roundToCurrencyPrecision(authorizedAmount, payment.currency_code)
)
const capture = await this.captureService_.create(
{
@@ -892,7 +920,7 @@ export default class PaymentModuleService
const paymentCollection = await this.paymentCollectionService_.retrieve(
paymentCollectionId,
{
select: ["amount", "raw_amount", "status"],
select: ["amount", "raw_amount", "status", "currency_code"],
relations: [
"payment_sessions.amount",
"payment_sessions.raw_amount",
@@ -938,12 +966,32 @@ export default class PaymentModuleService
: PaymentCollectionStatus.AWAITING
if (MathBN.gt(authorizedAmount, 0)) {
status = MathBN.gte(authorizedAmount, paymentCollection.amount)
status = MathBN.gte(
this.roundToCurrencyPrecision(
authorizedAmount,
paymentCollection.currency_code
),
this.roundToCurrencyPrecision(
paymentCollection.amount,
paymentCollection.currency_code
)
)
? PaymentCollectionStatus.AUTHORIZED
: PaymentCollectionStatus.PARTIALLY_AUTHORIZED
}
if (MathBN.eq(paymentCollection.amount, capturedAmount)) {
if (
MathBN.gte(
this.roundToCurrencyPrecision(
capturedAmount,
paymentCollection.currency_code
),
this.roundToCurrencyPrecision(
paymentCollection.amount,
paymentCollection.currency_code
)
)
) {
status = PaymentCollectionStatus.COMPLETED
completedAt = new Date()
}