From 70a72ce2df412a50ed7da1f326208599e9dc91c9 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:37:38 -0300 Subject: [PATCH] chore(payment-stripe): smallest unit (#7748) --- .../payment-stripe/src/core/stripe-base.ts | 30 +++++--- .../src/utils/get-smallest-unit.ts | 75 +++++++++++++++++++ 2 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 packages/modules/providers/payment-stripe/src/utils/get-smallest-unit.ts diff --git a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts index 6944184ce9..27b815f734 100644 --- a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts +++ b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts @@ -3,6 +3,7 @@ import { EOL } from "os" import Stripe from "stripe" import { + CreatePaymentProviderSession, MedusaContainer, PaymentProviderError, PaymentProviderSessionResponse, @@ -12,15 +13,12 @@ import { } from "@medusajs/types" import { AbstractPaymentProvider, - BigNumber, MedusaError, PaymentActions, PaymentSessionStatus, isDefined, isPaymentProviderError, } from "@medusajs/utils" - -import { CreatePaymentProviderSession } from "@medusajs/types" import { ErrorCodes, ErrorIntentStatus, @@ -28,6 +26,10 @@ import { StripeCredentials, StripeOptions, } from "../types" +import { + getAmountFromSmallestUnit, + getSmallestUnit, +} from "../utils/get-smallest-unit" abstract class StripeBase extends AbstractPaymentProvider { protected readonly options_: StripeOptions @@ -116,7 +118,7 @@ abstract class StripeBase extends AbstractPaymentProvider { const intentRequest: Stripe.PaymentIntentCreateParams = { description, - amount: Math.round(new BigNumber(amount).numeric), + amount: getSmallestUnit(amount, currency_code), currency: currency_code, metadata: { resource_id: resource_id ?? "Medusa Payment" }, capture_method: this.options_.capture ? "automatic" : "manual", @@ -232,8 +234,9 @@ abstract class StripeBase extends AbstractPaymentProvider { const id = paymentSessionData.id as string try { + const { currency } = paymentSessionData await this.stripe_.refunds.create({ - amount: Math.round(refundAmount), + amount: getSmallestUnit(refundAmount, currency as string), payment_intent: id as string, }) } catch (e) { @@ -249,6 +252,9 @@ abstract class StripeBase extends AbstractPaymentProvider { try { const id = paymentSessionData.id as string const intent = await this.stripe_.paymentIntents.retrieve(id) + + intent.amount = getAmountFromSmallestUnit(intent.amount, intent.currency) + return intent as unknown as PaymentProviderSessionResponse["data"] } catch (e) { return this.buildError("An error occurred in retrievePayment", e) @@ -258,9 +264,9 @@ abstract class StripeBase extends AbstractPaymentProvider { async updatePayment( input: UpdatePaymentProviderSession ): Promise { - const { context, data, amount } = input + const { context, data, currency_code, amount } = input - const amountNumeric = Math.round(new BigNumber(amount).numeric) + const amountNumeric = getSmallestUnit(amount, currency_code) const stripeId = context.customer?.metadata?.stripe_id @@ -317,13 +323,17 @@ abstract class StripeBase extends AbstractPaymentProvider { const event = this.constructWebhookEvent(webhookData) const intent = event.data.object as Stripe.PaymentIntent + const { currency } = intent switch (event.type) { case "payment_intent.amount_capturable_updated": return { action: PaymentActions.AUTHORIZED, data: { resource_id: intent.metadata.resource_id, - amount: intent.amount_capturable, // NOTE: revisit when implementing multicapture + amount: getAmountFromSmallestUnit( + intent.amount_capturable, + currency + ), // NOTE: revisit when implementing multicapture }, } case "payment_intent.succeeded": @@ -331,7 +341,7 @@ abstract class StripeBase extends AbstractPaymentProvider { action: PaymentActions.SUCCESSFUL, data: { resource_id: intent.metadata.resource_id, - amount: intent.amount_received, + amount: getAmountFromSmallestUnit(intent.amount_received, currency), }, } case "payment_intent.payment_failed": @@ -339,7 +349,7 @@ abstract class StripeBase extends AbstractPaymentProvider { action: PaymentActions.FAILED, data: { resource_id: intent.metadata.resource_id, - amount: intent.amount, + amount: getAmountFromSmallestUnit(intent.amount, currency), }, } default: diff --git a/packages/modules/providers/payment-stripe/src/utils/get-smallest-unit.ts b/packages/modules/providers/payment-stripe/src/utils/get-smallest-unit.ts new file mode 100644 index 0000000000..de4e36788a --- /dev/null +++ b/packages/modules/providers/payment-stripe/src/utils/get-smallest-unit.ts @@ -0,0 +1,75 @@ +import { BigNumberInput } from "@medusajs/types" +import { BigNumber, MathBN } from "@medusajs/utils" + +function getCurrencyMultiplier(currency) { + const currencyMultipliers = { + 0: [ + "BIF", + "CLP", + "DJF", + "GNF", + "JPY", + "KMF", + "KRW", + "MGA", + "PYG", + "RWF", + "UGX", + "VND", + "VUV", + "XAF", + "XOF", + "XPF", + ], + 3: ["BHD", "IQD", "JOD", "KWD", "OMR", "TND"], + } + + currency = currency.toUpperCase() + let power = 2 + for (const [key, value] of Object.entries(currencyMultipliers)) { + if (value.includes(currency)) { + power = parseInt(key, 10) + break + } + } + return Math.pow(10, power) +} + +/** + * Converts an amount to the format required by Stripe based on currency. + * https://docs.stripe.com/currencies + * @param {BigNumberInput} amount - The amount to be converted. + * @param {string} currency - The currency code (e.g., 'USD', 'JOD'). + * @returns {number} - The converted amount in the smallest currency unit. + */ +export function getSmallestUnit( + amount: BigNumberInput, + currency: string +): number { + const multiplier = getCurrencyMultiplier(currency) + const smallestAmount = new BigNumber(MathBN.mult(amount, multiplier)) + + let numeric = smallestAmount.numeric + + // Check if the currency requires rounding to the nearest ten + if (multiplier === 1e3) { + numeric = Math.ceil(numeric / 10) * 10 + } + + return numeric +} + +/** + * Converts an amount from the smallest currency unit to the standard unit based on currency. + * @param {BigNumberInput} amount - The amount in the smallest currency unit. + * @param {string} currency - The currency code (e.g., 'USD', 'JOD'). + * @returns {number} - The converted amount in the standard currency unit. + */ +export function getAmountFromSmallestUnit( + amount: BigNumberInput, + currency: string +): number { + const multiplier = getCurrencyMultiplier(currency) + const standardAmount = new BigNumber(MathBN.div(amount, multiplier)) + return standardAmount.numeric +}