From 3972e0965f66f62aaf8ab623bc2be1aed6123a87 Mon Sep 17 00:00:00 2001 From: Derek Wene Date: Thu, 14 Aug 2025 07:34:19 -0500 Subject: [PATCH 1/5] wip: braintree import refactor --- .vscode/settings.json | 7 +- .../src/core/braintree-base.ts | 315 ++++++++++-------- 2 files changed, 182 insertions(+), 140 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b02edc5..ad23fc31 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,8 @@ "editor.codeActionsOnSave": { "quickfix.biome": "explicit", }, - "editor.formatOnSave": true -} + "editor.formatOnSave": true, + "cSpell.words": [ + "Braintree" + ] +} \ No newline at end of file diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts index 47384dd8..aa7bcedd 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts @@ -7,7 +7,7 @@ import { PaymentSessionStatus, isDefined, } from '@medusajs/framework/utils'; -import jsonwebtoken from 'jsonwebtoken'; +import { z } from 'zod'; import type { AuthorizePaymentInput, AuthorizePaymentOutput, @@ -54,6 +54,14 @@ export interface BraintreePaymentSessionData { currency_code: string; paymentMethodNonce?: string; braintreeTransaction?: Transaction; + refundedTotal?: number; + importRefundedAmount?: number; +} + +export interface BraintreeInitiatePaymentData { + transactionId?: string; + previouslyRefundedAmount?: number; + paymentMethodNonce?: string; } const buildTokenCacheKey = (customerId: string) => `braintree:clientToken:${customerId}`; @@ -148,6 +156,7 @@ class BraintreeBase extends AbstractPaymentProvider { static validateOptions(options: BraintreeOptions): void { // Required string fields const requiredFields = ['merchantId', 'publicKey', 'privateKey', 'webhookSecret', 'environment']; + for (const field of requiredFields) { if (!isDefined(options[field]) || typeof options[field] !== 'string') { throw new MedusaError( @@ -157,7 +166,6 @@ class BraintreeBase extends AbstractPaymentProvider { } } - // Validate environment value const validEnvironments = ['qa', 'sandbox', 'production', 'development']; if (!validEnvironments.includes(options.environment.toLowerCase())) { throw new MedusaError( @@ -185,13 +193,13 @@ class BraintreeBase extends AbstractPaymentProvider { async capturePayment(input: CapturePaymentInput): Promise { const sessionData = await this.parsePaymentSessionData(input.data ?? {}); - const braintreeTransaction = sessionData.braintreeTransaction; + const transaction = sessionData.braintreeTransaction; - if (!braintreeTransaction) { + if (!transaction) { throw new MedusaError(MedusaError.Types.NOT_FOUND, 'Braintree transaction not found'); } - const paymentsResponse = await this.gateway.transaction.find(braintreeTransaction.id); + const paymentsResponse = await this.gateway.transaction.find(transaction.id); const authorized: TransactionStatus = 'authorized'; const submitted_for_settlement: TransactionStatus = 'submitted_for_settlement'; @@ -206,30 +214,27 @@ class BraintreeBase extends AbstractPaymentProvider { const captureResult = await this.gateway.transaction.submitForSettlement(id, toPay.toString()); if (captureResult.success) { - const transaction = await this.retrieveTransaction(braintreeTransaction.id); + const braintreeTransaction = await this.retrieveTransaction(transaction.id); const capturePaymentResult: CapturePaymentOutput = { data: { ...input.data, - braintreeTransaction: transaction, + braintreeTransaction, }, }; return capturePaymentResult; } - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `No payments found for transaction ${braintreeTransaction.id}`, - ); + throw new MedusaError(MedusaError.Types.NOT_FOUND, `No payments found for transaction ${transaction.id}`); } case settled: case settling: case submitted_for_settlement: { - const syncResult = await this.retrieveTransaction(braintreeTransaction.id); + const braintreeTransaction = await this.retrieveTransaction(transaction.id); const result: CapturePaymentOutput = { data: { ...input.data, - braintreeTransaction: syncResult.braintreeTransaction, + braintreeTransaction, }, }; @@ -237,7 +242,7 @@ class BraintreeBase extends AbstractPaymentProvider { } default: { - throw new MedusaError(MedusaError.Types.NOT_FOUND, `Not in a state to settle ${braintreeTransaction.id}`); + throw new MedusaError(MedusaError.Types.NOT_FOUND, `Not in a state to settle ${transaction.id}`); } } } @@ -246,35 +251,26 @@ class BraintreeBase extends AbstractPaymentProvider { try { const sessionData = await this.parsePaymentSessionData(input.data ?? {}); - this.logger.warn(`authorizePayment: ${JSON.stringify(sessionData)}`); - - let transaction: Transaction | undefined = sessionData.braintreeTransaction; + let braintreeTransaction = sessionData.braintreeTransaction; if (!sessionData.paymentMethodNonce) throw new MedusaError(MedusaError.Types.INVALID_ARGUMENT, 'Payment method nonce is required'); - if (!transaction) { - const braintreeTransaction = await this.createTransaction({ + if (!braintreeTransaction) + braintreeTransaction = await this.createTransaction({ input, }); - if (!braintreeTransaction) { - throw new MedusaError(MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR, 'Braintree transaction failed'); - } - transaction = braintreeTransaction; - } const paymentStatusRequest: GetPaymentStatusInput = { ...input, data: { ...input.data, - braintreeTransaction: transaction, + braintreeTransaction, }, }; const status = await this.getPaymentStatus(paymentStatusRequest); - this.logger.warn(`authorizePayment status: ${JSON.stringify(status)}`); - if (status.status === 'authorized' && this.options_.autoCapture) { status.status = 'captured'; } @@ -282,7 +278,7 @@ class BraintreeBase extends AbstractPaymentProvider { return { data: { ...input.data, - braintreeTransaction: transaction, + braintreeTransaction, }, status: status.status, }; @@ -294,7 +290,7 @@ class BraintreeBase extends AbstractPaymentProvider { async cancelPayment(input: CancelPaymentInput): Promise { const sessionData = await this.parsePaymentSessionData(input.data ?? {}); - const { braintreeTransaction } = await this.retrieveTransaction(sessionData.braintreeTransaction?.id as string); + const braintreeTransaction = await this.retrieveTransaction(sessionData.braintreeTransaction?.id as string); if (!braintreeTransaction) { return {}; @@ -307,14 +303,15 @@ class BraintreeBase extends AbstractPaymentProvider { ) { const updatedTransaction = await this.gateway.transaction.void(braintreeTransaction.id as string); if (updatedTransaction) { - const result = await this.retrieveTransaction(braintreeTransaction.id); + const updated = await this.retrieveTransaction(braintreeTransaction.id); return { data: { ...input.data, - braintreeTransaction: result.braintreeTransaction, + braintreeTransaction: updated, }, }; } + throw new MedusaError( MedusaError.Types.NOT_FOUND, `No payments found for transaction ${braintreeTransaction.id}`, @@ -340,7 +337,6 @@ class BraintreeBase extends AbstractPaymentProvider { const billingAddress = customer?.billing_address; const transactionRequest: Braintree.TransactionRequest = { amount: amount.toString(), - billing: { company: billingAddress?.company ?? '', streetAddress: billingAddress?.address_1 ?? '', @@ -367,30 +363,58 @@ class BraintreeBase extends AbstractPaymentProvider { return transactionRequest; } - async retrieveTransaction(braintreeTransactionId: string): Promise<{ - braintreeTransaction: Transaction; - }> { + private async retrieveTransaction( + braintreeTransactionId: string, + throwOnMissing: boolean = true, + ): Promise { const transactionData = await this.gateway.transaction.find(braintreeTransactionId); - return { - braintreeTransaction: transactionData as Transaction, - }; + if (!transactionData && throwOnMissing) + throw new MedusaError(MedusaError.Types.NOT_FOUND, `Braintree transaction not found: ${braintreeTransactionId}`); + + return transactionData; + } + + private validateInitiatePaymentData(data: Record): BraintreeInitiatePaymentData { + const schema = z.object({ + transactionId: z.string().optional(), + previouslyRefundedAmount: z.number().optional(), + paymentMethodNonce: z.string().optional(), + }); + + const result = schema.safeParse(data); + + if (!result.success) { + throw new MedusaError(MedusaError.Types.INVALID_ARGUMENT, result.error.message); + } + + return result.data; } async initiatePayment(input: InitiatePaymentInput): Promise { + const data = this.validateInitiatePaymentData(input.data ?? {}); + + let braintreeTransaction: Transaction | undefined; + + if (data.transactionId) braintreeTransaction = await this.retrieveTransaction(data.transactionId); + const token = await this.getValidClientToken(input.context?.customer?.id as string); - const paymentSessionId = input.context?.idempotency_key; + + const paymentSessionId = input.context?.idempotency_key as string; + if (!token) { throw new MedusaError(MedusaError.Types.INVALID_ARGUMENT, 'Failed to generate client token'); } - const { data } = input; const dataToSave: BraintreePaymentSessionData & { medusaPaymentSessionId: string } = { + braintreeTransaction, clientToken: token, medusaPaymentSessionId: paymentSessionId as string, paymentMethodNonce: data?.paymentMethodNonce as string, amount: getSmallestUnit(input.amount, input.currency_code), currency_code: input.currency_code, + importRefundedAmount: data.previouslyRefundedAmount, + refundedTotal: 0, }; return { @@ -429,8 +453,7 @@ class BraintreeBase extends AbstractPaymentProvider { } try { - const result = await this.retrieveTransaction(braintreeTransaction.transaction.id); - return result.braintreeTransaction; + return await this.retrieveTransaction(braintreeTransaction.transaction.id); } catch (error) { this.logger.error(`Error syncing payment session: ${error.message}`, error); if (braintreeTransaction.transaction?.id) { @@ -444,94 +467,6 @@ class BraintreeBase extends AbstractPaymentProvider { } } - private async createBraintreeCustomer(customer: PaymentCustomerDTO): Promise { - const customerResult = await this.gateway.customer.create({ - email: customer.email, - firstName: customer.first_name ?? undefined, - lastName: customer.last_name ?? undefined, - phone: customer.phone ?? undefined, - }); - - if (!customerResult.success) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Failed to create Braintree customer: ${JSON.stringify(customerResult.errors)}`, - ); - } - - return customerResult.customer; - } - - async createAccountHolder(input: CreateAccountHolderInput): Promise { - const customer = await this.createBraintreeCustomer(input.context.customer); - - return { - id: customer.id, - data: { - ...customer, - }, - }; - } - - async updateAccountHolder(input: UpdateAccountHolderInput): Promise { - const { context } = input; - const accountHolderId = context.account_holder?.data?.id as string; - if (!accountHolderId) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, `Account holder id is required`); - } - try { - const accountHolder = await this.gateway.customer.find(accountHolderId); - if (!accountHolder) { - throw new MedusaError(MedusaError.Types.NOT_FOUND, `Account holder with id ${accountHolderId} not found`); - } - const customerUpdateRequest: Braintree.CustomerUpdateRequest = { - firstName: input.context.customer?.first_name ?? accountHolder.firstName, - lastName: input.context.customer?.last_name ?? accountHolder.lastName, - email: input.context.customer?.email ?? accountHolder.email, - phone: input.context.customer?.phone ?? accountHolder.phone, - }; - - const updateResult = await this.gateway.customer.update(accountHolder.id, customerUpdateRequest); - - if (!updateResult.success) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Failed to update account holder: ${JSON.stringify(updateResult.errors)}`, - ); - } - - return { - data: { ...updateResult.customer }, - }; - } catch (e) { - this.logger.error(`Error updating account holder: ${e.message}`, e); - throw new MedusaError(MedusaError.Types.INVALID_DATA, `Failed to update account holder: ${e.message}`); - } - } - - async deleteAccountHolder(input: DeleteAccountHolderInput): Promise { - const { context } = input; - const accountHolderId = context.account_holder?.data?.id as string; - if (!accountHolderId) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, `Account holder id is required`); - } - try { - const accountHolder = await this.gateway.customer.find(accountHolderId); - if (!accountHolder) { - throw new MedusaError(MedusaError.Types.NOT_FOUND, `Account holder with id ${accountHolderId} not found`); - } - - await this.gateway.customer.delete(accountHolder.id); - - return { - data: {}, - }; - } catch (e) { - this.logger.error(`Error deleting account holder: ${e.message}`, e); - throw new MedusaError(MedusaError.Types.INVALID_DATA, `Failed to delete account holder: ${e.message}`); - } - } - async deletePayment(input: DeletePaymentInput): Promise { const sessionData = await this.parsePaymentSessionData(input.data ?? {}); const braintreeTransaction = sessionData.braintreeTransaction; @@ -652,12 +587,26 @@ class BraintreeBase extends AbstractPaymentProvider { async refundPayment(input: RefundPaymentInput): Promise { const sessionData = await this.parsePaymentSessionData(input.data ?? {}); - const { braintreeTransaction } = await this.retrieveTransaction(sessionData.braintreeTransaction?.id as string); - if (!braintreeTransaction) { - throw new MedusaError(MedusaError.Types.NOT_FOUND, 'Braintree transaction not found'); + const refundAmount = getSmallestUnit(input.amount, sessionData.currency_code); + + const previouslyRefundedAmount = sessionData.refundedTotal ?? 0; + + if (sessionData.importRefundedAmount && refundAmount === sessionData.importRefundedAmount) { + // This is a hack so that we can import braintree transactions that have already been refunded. + // We basically skip the refund process here and just pretend that we refunded the amount. + const updatedTransaction = await this.retrieveTransaction(sessionData.braintreeTransaction!.id); + + const refundResult: RefundPaymentOutput = { + data: { + ...input.data, + braintreeTransaction: updatedTransaction, + refundedTotal: previouslyRefundedAmount + refundAmount, + }, + }; + return refundResult; } - const refundAmount = getSmallestUnit(input.amount, sessionData.currency_code); + const braintreeTransaction = await this.retrieveTransaction(sessionData.braintreeTransaction?.id as string); if (braintreeTransaction.status === 'submitted_for_settlement' || braintreeTransaction.status === 'authorized') { const cancelledTransaction = await this.gateway.transaction.void(braintreeTransaction.id); @@ -667,10 +616,11 @@ class BraintreeBase extends AbstractPaymentProvider { const refundResult: RefundPaymentOutput = { data: { ...input.data, - braintreeTransaction: result.braintreeTransaction, + braintreeTransaction, braintreeRefund: cancelledTransaction, }, }; + return refundResult; } @@ -691,13 +641,14 @@ class BraintreeBase extends AbstractPaymentProvider { refundRequest.amount.toString(), ); - const result = await this.retrieveTransaction(braintreeTransaction.id); + const updatedTransaction = await this.retrieveTransaction(braintreeTransaction.id); const refundResult: RefundPaymentOutput = { data: { ...input.data, - braintreeTransaction: result.braintreeTransaction, + braintreeTransaction: updatedTransaction, braintreeRefund: refundTransaction, + refundedTotal: previouslyRefundedAmount + refundAmount, }, }; return refundResult; @@ -727,7 +678,7 @@ class BraintreeBase extends AbstractPaymentProvider { return { data: { ...input.data, - braintreeTransaction: retrieved.braintreeTransaction, + braintreeTransaction: retrieved, }, }; } @@ -743,6 +694,76 @@ class BraintreeBase extends AbstractPaymentProvider { }; } + async createAccountHolder(input: CreateAccountHolderInput): Promise { + const customer = await this.createBraintreeCustomer(input.context.customer); + + return { + id: customer.id, + data: { + ...customer, + }, + }; + } + + async updateAccountHolder(input: UpdateAccountHolderInput): Promise { + const { context } = input; + const accountHolderId = context.account_holder?.data?.id as string; + if (!accountHolderId) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, `Account holder id is required`); + } + try { + const accountHolder = await this.gateway.customer.find(accountHolderId); + if (!accountHolder) { + throw new MedusaError(MedusaError.Types.NOT_FOUND, `Account holder with id ${accountHolderId} not found`); + } + const customerUpdateRequest: Braintree.CustomerUpdateRequest = { + firstName: input.context.customer?.first_name ?? accountHolder.firstName, + lastName: input.context.customer?.last_name ?? accountHolder.lastName, + email: input.context.customer?.email ?? accountHolder.email, + phone: input.context.customer?.phone ?? accountHolder.phone, + }; + + const updateResult = await this.gateway.customer.update(accountHolder.id, customerUpdateRequest); + + if (!updateResult.success) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Failed to update account holder: ${JSON.stringify(updateResult.errors)}`, + ); + } + + return { + data: { ...updateResult.customer }, + }; + } catch (e) { + this.logger.error(`Error updating account holder: ${e.message}`, e); + throw new MedusaError(MedusaError.Types.INVALID_DATA, `Failed to update account holder: ${e.message}`); + } + } + + async deleteAccountHolder(input: DeleteAccountHolderInput): Promise { + const { context } = input; + const accountHolderId = context.account_holder?.data?.id as string; + if (!accountHolderId) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, `Account holder id is required`); + } + try { + const accountHolder = await this.gateway.customer.find(accountHolderId); + if (!accountHolder) { + throw new MedusaError(MedusaError.Types.NOT_FOUND, `Account holder with id ${accountHolderId} not found`); + } + + await this.gateway.customer.delete(accountHolder.id); + + return { + data: {}, + }; + } catch (e) { + this.logger.error(`Error deleting account holder: ${e.message}`, e); + throw new MedusaError(MedusaError.Types.INVALID_DATA, `Failed to delete account holder: ${e.message}`); + } + } + async getWebhookActionAndData(webhookData: ProviderWebhookPayload['payload']): Promise { const logger = this.logger; @@ -791,6 +812,24 @@ class BraintreeBase extends AbstractPaymentProvider { return { action: PaymentActions.NOT_SUPPORTED }; } } + + private async createBraintreeCustomer(customer: PaymentCustomerDTO): Promise { + const customerResult = await this.gateway.customer.create({ + email: customer.email, + firstName: customer.first_name ?? undefined, + lastName: customer.last_name ?? undefined, + phone: customer.phone ?? undefined, + }); + + if (!customerResult.success) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Failed to create Braintree customer: ${JSON.stringify(customerResult.errors)}`, + ); + } + + return customerResult.customer; + } } export default BraintreeBase; From a63593f8da0a2bfb4e3e20a1fb46d84a2f6b8736 Mon Sep 17 00:00:00 2001 From: Derek Wene Date: Thu, 14 Aug 2025 11:21:03 -0500 Subject: [PATCH 2/5] feat: add braintree integration tests --- plugins/braintree-payment/jest.config.cjs | 20 + plugins/braintree-payment/package.json | 6 +- .../src/core/__tests__/braintree-base.spec.ts | 214 ++ .../src/core/braintree-base.ts | 188 +- .../src/services/braintree-provider.ts | 7 +- .../src/utils/get-smallest-unit.ts | 29 + plugins/braintree-payment/tsconfig.spec.json | 25 + yarn.lock | 1862 ++++++++++++++++- 8 files changed, 2150 insertions(+), 201 deletions(-) create mode 100644 plugins/braintree-payment/jest.config.cjs create mode 100644 plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts create mode 100644 plugins/braintree-payment/tsconfig.spec.json diff --git a/plugins/braintree-payment/jest.config.cjs b/plugins/braintree-payment/jest.config.cjs new file mode 100644 index 00000000..5f3eee1f --- /dev/null +++ b/plugins/braintree-payment/jest.config.cjs @@ -0,0 +1,20 @@ +/** @type {import('jest').Config} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.spec.ts', '**/*.spec.ts'], + transform: { + '^.+\\.(ts|tsx)$': [ + 'ts-jest', + { tsconfig: '/tsconfig.spec.json', diagnostics: false, isolatedModules: true } + ], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], + verbose: false, + collectCoverage: false, + // Ignore compiled medusa build output + testPathIgnorePatterns: ['/node_modules/', '/.medusa/'], +}; + + diff --git a/plugins/braintree-payment/package.json b/plugins/braintree-payment/package.json index 590a2c74..1f859303 100644 --- a/plugins/braintree-payment/package.json +++ b/plugins/braintree-payment/package.json @@ -47,11 +47,15 @@ "@medusajs/ui": "^4.0.3", "@swc/core": "1.5.7", "@types/braintree": "^3.3.14", + "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.10", - "jsonwebtoken": "^9.0.2" + "jest": "^29.7.0", + "jsonwebtoken": "^9.0.2", + "ts-jest": "^29.2.5" }, "scripts": { "build": "npx medusa plugin:build", + "test": "jest --config jest.config.cjs --runInBand", "plugin:dev": "npx medusa plugin:develop", "prepublishOnly": "npx medusa plugin:build" }, diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts new file mode 100644 index 00000000..38c0db64 --- /dev/null +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts @@ -0,0 +1,214 @@ +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +// Minimal fallbacks to avoid depending on framework type packages in tests +const ContainerRegistrationKeys = { LOGGER: 'logger' } as const; +const Modules = { CACHE: 'cache' } as const; +import BraintreeProviderService from '../../services/braintree-provider'; +import { MedusaContainer } from '@medusajs/framework/types'; +import { BraintreeConstructorArgs, BraintreePaymentSessionData } from '../braintree-base'; + +const buildService = () => { + const logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() } as any; + const cache = { get: jest.fn(), set: jest.fn() } as any; + + const container: BraintreeConstructorArgs = { + logger, + cache, + }; + + const options = { + environment: 'sandbox' as const, + merchantId: 'merchant', + publicKey: 'public', + privateKey: 'private', + enable3DSecure: false, + savePaymentMethod: false, + webhookSecret: 'whsec', + autoCapture: true, + } as any; // satisfy BraintreeOptions without bringing full type deps + + const service = new BraintreeProviderService(container, options); + + // Replace gateway with a mock implementation + const gateway = { + clientToken: { generate: jest.fn() }, + transaction: { + sale: jest.fn(), + find: jest.fn(), + submitForSettlement: jest.fn(), + void: jest.fn(), + refund: jest.fn(), + }, + paymentMethod: { create: jest.fn() }, + customer: { find: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn() }, + webhookNotification: { parse: jest.fn() }, + } as any; + + (service as any).gateway = gateway; + + return { service, gateway, logger, cache }; +}; + +describe('BraintreeProviderService core behaviors', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('returns cached client token when available', async () => { + const { service, gateway, cache } = buildService(); + cache.get.mockResolvedValueOnce('cached-token'); + + const token = await (service as any).getValidClientToken('cust_1'); + + expect(token).toBe('cached-token'); + expect(gateway.clientToken.generate).not.toHaveBeenCalled(); + }); + + it('generates and caches client token when missing, with correct TTL', async () => { + const { service, gateway, cache } = buildService(); + jest.useFakeTimers(); + jest.setSystemTime(new Date('2020-01-01T00:00:00Z')); + + cache.get.mockResolvedValueOnce(null); + gateway.clientToken.generate.mockResolvedValueOnce({ clientToken: 'new-token' }); + + const token = await (service as any).getValidClientToken('cust_2'); + + expect(token).toBe('new-token'); + expect(cache.set).toHaveBeenCalled(); + const setArgs = cache.set.mock.calls[0]; + // [key, value, ttlSeconds] + expect(setArgs[1]).toBe('new-token'); + // 24h - 1s + expect(setArgs[2]).toBe(24 * 3600 - 1); + }); + + it('authorizePayment creates a sale with decimal string amount and returns captured when autoCapture=true', async () => { + const { service, gateway } = buildService(); + + const input = { + data: { + clientToken: 'ct', + amount: 1000, // smallest unit + currency_code: 'USD', + paymentMethodNonce: 'fake-nonce', + }, + context: { + idempotency_key: 'idem_1', + customer: { id: 'cust', email: 'c@example.com' }, + }, + } as any; + + gateway.transaction.sale.mockResolvedValueOnce({ success: true, transaction: { id: 't1' } }); + gateway.transaction.find.mockResolvedValue({ id: 't1', status: 'authorized', amount: '10.00' }); + + const result = await service.authorizePayment(input); + + expect(gateway.transaction.sale).toHaveBeenCalled(); + const saleArgs = gateway.transaction.sale.mock.calls[0][0]; + expect(saleArgs.amount).toBe('10.00'); + expect(result.status).toBe('captured'); + }); + + it('capturePayment submits for settlement when status is authorized', async () => { + const { service, gateway } = buildService(); + + const input = { + data: { + clientToken: 'ct', + amount: 1000, + currency_code: 'USD', + braintreeTransaction: { id: 't1' }, + }, + } as any; + + gateway.transaction.find + .mockResolvedValueOnce({ id: 't1', status: 'authorized', amount: '10.00' }) // pre-check + .mockResolvedValueOnce({ id: 't1', status: 'submitted_for_settlement', amount: '10.00' }); // retrieve after submit + gateway.transaction.submitForSettlement.mockResolvedValueOnce({ success: true }); + + const result = await service.capturePayment(input); + + expect(gateway.transaction.submitForSettlement).toHaveBeenCalledWith('t1', '10.00'); + + const data = result.data as unknown as BraintreePaymentSessionData; + expect(data?.braintreeTransaction?.id).toBe('t1'); + }); + + it('refundPayment voids when transaction is authorized', async () => { + const { service, gateway } = buildService(); + + const input = { + amount: 5, // standard unit, will be converted internally + data: { + clientToken: 'ct', + amount: 1000, + currency_code: 'USD', + braintreeTransaction: { id: 't1' }, + }, + } as any; + + gateway.transaction.find.mockResolvedValueOnce({ id: 't1', status: 'authorized' }); + gateway.transaction.void.mockResolvedValueOnce({ success: true }); + gateway.transaction.find.mockResolvedValueOnce({ id: 't1', status: 'voided' }); + + const result = await service.refundPayment(input); + + expect(gateway.transaction.void).toHaveBeenCalledWith('t1'); + expect((result.data as any)?.braintreeRefund?.success).toBe(true); + }); + + it('refundPayment refunds with decimal string when transaction is settled', async () => { + const { service, gateway } = buildService(); + + const input = { + amount: 5, // standard unit => 500 smallest => "5.00" decimal + data: { + clientToken: 'ct', + amount: 1000, + currency_code: 'USD', + braintreeTransaction: { id: 't2' }, + }, + } as any; + + gateway.transaction.find + .mockResolvedValueOnce({ id: 't2', status: 'settled' }) // retrieveTransaction + .mockResolvedValueOnce({ id: 't2', status: 'settled' }); // updated after refund + gateway.transaction.refund.mockResolvedValueOnce({ transaction: { id: 'r1' } }); + + const result = await service.refundPayment(input); + + expect(gateway.transaction.refund).toHaveBeenCalledWith('t2', '5.00'); + expect((result.data as any)?.braintreeRefund?.id).toBe('r1'); + }); + + it('getPaymentStatus maps provider status correctly', async () => { + const { service, gateway } = buildService(); + const input = { data: { braintreeTransaction: { id: 't3' } } } as any; + gateway.transaction.find.mockResolvedValueOnce({ id: 't3', status: 'failed' }); + + const result = await service.getPaymentStatus(input); + expect(result.status).toBe('error'); + }); + + it('getWebhookActionAndData returns successful for transaction_settled', async () => { + const { service, gateway } = buildService(); + const payloadStr = 'bt_signature=s&bt_payload=p'; + gateway.webhookNotification.parse.mockResolvedValueOnce({ + kind: 'transaction_settled', + transaction: { id: 't4' }, + }); + gateway.transaction.find.mockResolvedValueOnce({ + id: 't4', + amount: '12.34', + customFields: { medusa_payment_session_id: 'sess_123' }, + }); + + const result = await service.getWebhookActionAndData({ data: payloadStr } as any); + expect(result.action).toBe('captured'); + expect((result as any).data.session_id).toBe('sess_123'); + }); +}); diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts index aa7bcedd..108f274e 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts @@ -45,8 +45,13 @@ import type { } from '@medusajs/types'; import type { Transaction, TransactionNotification, TransactionStatus } from 'braintree'; import Braintree from 'braintree'; -import type { BraintreeOptions, CustomFields, DecodedClientToken, DecodedClientTokenAuthorization } from '../types'; -import { getSmallestUnit } from '../utils/get-smallest-unit'; +import type { BraintreeOptions, CustomFields } from '../types'; +import { getSmallestUnit, formatSmallestUnitToDecimalString } from '../utils/get-smallest-unit'; + +export type BraintreeConstructorArgs = Record & { + logger: Logger; + cache: ICacheService; +}; export interface BraintreePaymentSessionData { clientToken: string; @@ -71,24 +76,22 @@ class BraintreeBase extends AbstractPaymentProvider { protected readonly options_: BraintreeOptions; protected gateway: Braintree.BraintreeGateway; logger: Logger; - container_: MedusaContainer; cache: ICacheService; - protected constructor(container: MedusaContainer, options: BraintreeOptions) { + + protected constructor(container: BraintreeConstructorArgs, options: BraintreeOptions) { super(container, options); this.options_ = options; this.logger = container[ContainerRegistrationKeys.LOGGER]; - this.container_ = container; this.cache = container[Modules.CACHE] as unknown as ICacheService; this.init(); } - async saveClientTokenToCache(clientToken: string, customerId: string, expiresOn: number): Promise { - const expiryTime = expiresOn * 1000 - Math.floor(Date.now()) - 1000; - - if (!customerId || !clientToken || expiryTime < 0) return; - - await this.cache.set(buildTokenCacheKey(customerId), clientToken, Math.floor(expiryTime / 1000)); + async saveClientTokenToCache(clientToken: string, customerId: string, expiresOnEpochSeconds: number): Promise { + const nowSeconds = Math.floor(Date.now() / 1000); + const ttlSeconds = expiresOnEpochSeconds - nowSeconds - 1; + if (!customerId || !clientToken || ttlSeconds <= 0) return; + await this.cache.set(buildTokenCacheKey(customerId), clientToken, ttlSeconds); } async getClientTokenFromCache(customerId: string): Promise { @@ -107,46 +110,45 @@ class BraintreeBase extends AbstractPaymentProvider { if (token) return token; const generatedToken = await this.gateway.clientToken.generate({}); - const defaultExpiryTime = Math.floor(Date.now() / 1000) + 24 * 3600 * 1000; // 24 hours default + const defaultExpiryEpochSeconds = Math.floor(Date.now() / 1000) + 24 * 3600; // 24 hours default - await this.saveClientTokenToCache(generatedToken.clientToken, customerId, defaultExpiryTime); + await this.saveClientTokenToCache(generatedToken.clientToken, customerId, defaultExpiryEpochSeconds); return generatedToken.clientToken; } private async parsePaymentSessionData(data: Record): Promise { - return { - clientToken: data.clientToken as string, - amount: data.amount as number, - currency_code: data.currency_code as string, - paymentMethodNonce: data.paymentMethodNonce as string | undefined, - braintreeTransaction: data.braintreeTransaction as Transaction | undefined, - }; + const schema = z.object({ + clientToken: z.string(), + amount: z.number(), + currency_code: z.string(), + paymentMethodNonce: z.string().optional(), + braintreeTransaction: z.any().optional(), + refundedTotal: z.number().optional(), + importRefundedAmount: z.number().optional(), + }); + + const result = schema.safeParse(data); + if (!result.success) { + throw new MedusaError(MedusaError.Types.INVALID_ARGUMENT, result.error.message); + } + + return result.data as BraintreePaymentSessionData; } init(): void { - let environment: Braintree.Environment; - switch (this.options_.environment) { - case 'qa': - environment = Braintree.Environment.Qa; - break; - case 'sandbox': - environment = Braintree.Environment.Sandbox; - break; - case 'production': - environment = Braintree.Environment.Production; - break; - case 'development': - environment = Braintree.Environment.Development; - break; - default: - environment = Braintree.Environment.Sandbox; - break; - } + const envKey = (this.options_.environment || 'sandbox').toLowerCase(); + const envMap: Record = { + qa: Braintree.Environment.Qa, + sandbox: Braintree.Environment.Sandbox, + production: Braintree.Environment.Production, + development: Braintree.Environment.Development, + }; + const environment = envMap[envKey] ?? Braintree.Environment.Sandbox; this.gateway = this.gateway || new Braintree.BraintreeGateway({ - environment: environment, + environment, merchantId: this.options_.merchantId!, publicKey: this.options_.publicKey!, privateKey: this.options_.privateKey!, @@ -201,17 +203,12 @@ class BraintreeBase extends AbstractPaymentProvider { const paymentsResponse = await this.gateway.transaction.find(transaction.id); - const authorized: TransactionStatus = 'authorized'; - const submitted_for_settlement: TransactionStatus = 'submitted_for_settlement'; - const settled: TransactionStatus = 'settled'; - const settling: TransactionStatus = 'settling'; - switch (paymentsResponse.status) { - case authorized: { + case 'authorized': { const { id, amount } = paymentsResponse; const toPay = amount.toString(); - const captureResult = await this.gateway.transaction.submitForSettlement(id, toPay.toString()); + const captureResult = await this.gateway.transaction.submitForSettlement(id, toPay); if (captureResult.success) { const braintreeTransaction = await this.retrieveTransaction(transaction.id); @@ -226,9 +223,9 @@ class BraintreeBase extends AbstractPaymentProvider { } throw new MedusaError(MedusaError.Types.NOT_FOUND, `No payments found for transaction ${transaction.id}`); } - case settled: - case settling: - case submitted_for_settlement: { + case 'settled': + case 'settling': + case 'submitted_for_settlement': { const braintreeTransaction = await this.retrieveTransaction(transaction.id); const result: CapturePaymentOutput = { @@ -270,17 +267,14 @@ class BraintreeBase extends AbstractPaymentProvider { }; const status = await this.getPaymentStatus(paymentStatusRequest); - - if (status.status === 'authorized' && this.options_.autoCapture) { - status.status = 'captured'; - } + const finalStatus = status.status === 'authorized' && this.options_.autoCapture ? 'captured' : status.status; return { data: { ...input.data, braintreeTransaction, }, - status: status.status, + status: finalStatus, }; } catch (error) { this.logger.error(`Error authorizing payment: ${error.message}`, error); @@ -375,6 +369,30 @@ class BraintreeBase extends AbstractPaymentProvider { return transactionData; } + private mapTransactionStatusToPaymentStatus(status: TransactionStatus): PaymentSessionStatus { + switch (status) { + case 'authorization_expired': + return PaymentSessionStatus.CANCELED; + case 'authorizing': + return PaymentSessionStatus.REQUIRES_MORE; + case 'authorized': + return PaymentSessionStatus.AUTHORIZED; + case 'settled': + case 'settlement_confirmed': + return PaymentSessionStatus.CAPTURED; + case 'settling': + case 'settlement_pending': + case 'submitted_for_settlement': + return PaymentSessionStatus.AUTHORIZED; + case 'voided': + return PaymentSessionStatus.CANCELED; + case 'failed': + return PaymentSessionStatus.ERROR; + default: + return PaymentSessionStatus.PENDING; + } + } + private validateInitiatePaymentData(data: Record): BraintreeInitiatePaymentData { const schema = z.object({ transactionId: z.string().optional(), @@ -430,10 +448,10 @@ class BraintreeBase extends AbstractPaymentProvider { }): Promise { const sessionData = await this.parsePaymentSessionData(input.data ?? {}); - const toPay = sessionData.amount.toString(); + const toPayDecimal = formatSmallestUnitToDecimalString(sessionData.amount, sessionData.currency_code); const braintreeTransactionCreateRequest = this.getBraintreeTransactionCreateRequestBody({ - amount: toPay, + amount: toPayDecimal, nonce: sessionData.paymentMethodNonce as string, customFields: { medusa_payment_session_id: input.context?.idempotency_key, @@ -509,45 +527,7 @@ class BraintreeBase extends AbstractPaymentProvider { this.logger.warn('received payment data from session not transaction data'); throw e; } - let status: PaymentSessionStatus = PaymentSessionStatus.PENDING; - switch (transaction.status) { - case 'authorization_expired': - status = PaymentSessionStatus.CANCELED; - break; - case 'authorizing': - status = PaymentSessionStatus.REQUIRES_MORE; - break; - case 'authorized': - status = PaymentSessionStatus.AUTHORIZED; - break; - case 'settled': - status = PaymentSessionStatus.CAPTURED; - break; - case 'settling': - status = PaymentSessionStatus.AUTHORIZED; - break; - case 'settlement_confirmed': - status = PaymentSessionStatus.CAPTURED; - break; - case 'settlement_pending': - status = PaymentSessionStatus.AUTHORIZED; - break; - case 'submitted_for_settlement': - status = PaymentSessionStatus.AUTHORIZED; - break; - case 'voided': { - status = PaymentSessionStatus.CANCELED; - - break; - } - case 'failed': { - status = PaymentSessionStatus.ERROR; - - break; - } - default: - status = PaymentSessionStatus.PENDING; - } + const status = this.mapTransactionStatusToPaymentStatus(transaction.status); return { status }; } @@ -608,7 +588,8 @@ class BraintreeBase extends AbstractPaymentProvider { const braintreeTransaction = await this.retrieveTransaction(sessionData.braintreeTransaction?.id as string); - if (braintreeTransaction.status === 'submitted_for_settlement' || braintreeTransaction.status === 'authorized') { + const shouldVoid = ['submitted_for_settlement', 'authorized'].includes(braintreeTransaction.status); + if (shouldVoid) { const cancelledTransaction = await this.gateway.transaction.void(braintreeTransaction.id); const result = await this.retrieveTransaction(braintreeTransaction.id); @@ -624,7 +605,8 @@ class BraintreeBase extends AbstractPaymentProvider { return refundResult; } - if (braintreeTransaction.status !== 'settled' && braintreeTransaction.status !== 'settling') { + const shouldRefund = ['settled', 'settling'].includes(braintreeTransaction.status); + if (!shouldRefund) { throw new MedusaError( MedusaError.Types.NOT_FOUND, `Braintree transaction with ID ${braintreeTransaction.id} cannot be refunded`, @@ -632,13 +614,11 @@ class BraintreeBase extends AbstractPaymentProvider { } if (braintreeTransaction.id) { - const refundRequest = { - amount: refundAmount, - }; + const refundAmountDecimal = formatSmallestUnitToDecimalString(refundAmount, sessionData.currency_code); try { const { transaction: refundTransaction } = await this.gateway.transaction.refund( braintreeTransaction.id, - refundRequest.amount.toString(), + refundAmountDecimal, ); const updatedTransaction = await this.retrieveTransaction(braintreeTransaction.id); @@ -684,14 +664,14 @@ class BraintreeBase extends AbstractPaymentProvider { } // biome-ignore lint/suspicious/useAwait: - async updatePayment(input: UpdatePaymentInput): Promise { - return { + updatePayment(input: UpdatePaymentInput): Promise { + return Promise.resolve({ data: { ...input.data, amount: input.amount, currency_code: input.currency_code, }, - }; + }); } async createAccountHolder(input: CreateAccountHolderInput): Promise { diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-provider.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-provider.ts index 3c2fd10c..06b7e39a 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-provider.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-provider.ts @@ -1,13 +1,12 @@ -import type { MedusaContainer } from '@medusajs/medusa'; -import BraintreeBase from '../core/braintree-base'; -import { PaymentProviderKeys } from '../types'; +import BraintreeBase, { BraintreeConstructorArgs } from '../core/braintree-base'; import type { BraintreeOptions } from '../types'; +import { PaymentProviderKeys } from '../types'; class BraintreeProviderService extends BraintreeBase { static identifier = PaymentProviderKeys.BRAINTREE; options: BraintreeOptions; - constructor(container: MedusaContainer, options: BraintreeOptions) { + constructor(container: BraintreeConstructorArgs, options: BraintreeOptions) { super(container, options); this.options = options; } diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/utils/get-smallest-unit.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/utils/get-smallest-unit.ts index 32dd8115..461f48dc 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/utils/get-smallest-unit.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/utils/get-smallest-unit.ts @@ -60,3 +60,32 @@ export function getAmountFromSmallestUnit(amount: BigNumberInput, currency: stri const standardAmount = new BigNumber(MathBN.div(amount, multiplier)); return standardAmount.numeric; } + +/** + * Formats an amount provided in the smallest currency unit into a decimal string + * suitable for providers (e.g., Braintree) that expect standard unit decimal strings. + * + * Examples: + * - USD: 1234 -> "12.34" + * - JPY: 1234 -> "1234" + * - JOD (3 decimals): 12340 -> "12.340" + */ +export function formatSmallestUnitToDecimalString(amount: BigNumberInput, currency: string): string { + const multiplier = getCurrencyMultiplier(currency); + + // Determine number of fraction digits based on multiplier (10^digits) + let fractionDigits = 0; + if (multiplier === 1000) { + fractionDigits = 3; + } else if (multiplier === 100) { + fractionDigits = 2; + } else if (multiplier === 1) { + fractionDigits = 0; + } else { + // Fallback: infer by counting zeros in multiplier + fractionDigits = Math.max(0, Math.round(Math.log10(multiplier))); + } + + const standardAmount = getAmountFromSmallestUnit(amount, currency); + return standardAmount.toFixed(fractionDigits); +} diff --git a/plugins/braintree-payment/tsconfig.spec.json b/plugins/braintree-payment/tsconfig.spec.json new file mode 100644 index 00000000..1b026cd6 --- /dev/null +++ b/plugins/braintree-payment/tsconfig.spec.json @@ -0,0 +1,25 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types": [ + "jest", + "node" + ], + "jsx": "react-jsx", + "allowJs": false, + "module": "Node16", + "moduleResolution": "Node16", + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "exclude": [ + "node_modules", + ".medusa/server", + ".medusa/admin" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5d134b26..bd5a9909 100644 --- a/yarn.lock +++ b/yarn.lock @@ -750,7 +750,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.24.7, @babel/code-frame@npm:^7.27.1": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.24.7, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" dependencies: @@ -768,6 +768,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.23.9": + version: 7.28.3 + resolution: "@babel/core@npm:7.28.3" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.28.3" + "@babel/helper-compilation-targets": "npm:^7.27.2" + "@babel/helper-module-transforms": "npm:^7.28.3" + "@babel/helpers": "npm:^7.28.3" + "@babel/parser": "npm:^7.28.3" + "@babel/template": "npm:^7.27.2" + "@babel/traverse": "npm:^7.28.3" + "@babel/types": "npm:^7.28.2" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10c0/e6b3eb830c4b93f5a442b305776df1cd2bb4fafa4612355366f67c764f3e54a69d45b84def77fb2d4fd83439102667b0a92c3ea2838f678733245b748c602a7b + languageName: node + linkType: hard + "@babel/core@npm:^7.28.0": version: 7.28.0 resolution: "@babel/core@npm:7.28.0" @@ -804,6 +827,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.28.3, @babel/generator@npm:^7.7.2": + version: 7.28.3 + resolution: "@babel/generator@npm:7.28.3" + dependencies: + "@babel/parser": "npm:^7.28.3" + "@babel/types": "npm:^7.28.2" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10c0/0ff58bcf04f8803dcc29479b547b43b9b0b828ec1ee0668e92d79f9e90f388c28589056637c5ff2fd7bcf8d153c990d29c448d449d852bf9d1bc64753ca462bc + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.27.2": version: 7.27.2 resolution: "@babel/helper-compilation-targets@npm:7.27.2" @@ -847,7 +883,20 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.27.1": +"@babel/helper-module-transforms@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/helper-module-transforms@npm:7.28.3" + dependencies: + "@babel/helper-module-imports": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.3" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/549be62515a6d50cd4cfefcab1b005c47f89bd9135a22d602ee6a5e3a01f27571868ada10b75b033569f24dc4a2bb8d04bfa05ee75c16da7ade2d0db1437fcdb + languageName: node + linkType: hard + +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.27.1, @babel/helper-plugin-utils@npm:^7.8.0": version: 7.27.1 resolution: "@babel/helper-plugin-utils@npm:7.27.1" checksum: 10c0/94cf22c81a0c11a09b197b41ab488d416ff62254ce13c57e62912c85700dc2e99e555225787a4099ff6bae7a1812d622c80fbaeda824b79baa10a6c5ac4cf69b @@ -885,6 +934,16 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/helpers@npm:7.28.3" + dependencies: + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.28.2" + checksum: 10c0/03a8f94135415eec62d37be9c62c63908f2d5386c7b00e04545de4961996465775330e3eb57717ea7451e19b0e24615777ebfec408c2adb1df3b10b4df6bf1ce + languageName: node + linkType: hard + "@babel/parser@npm:7.25.6": version: 7.25.6 resolution: "@babel/parser@npm:7.25.6" @@ -907,6 +966,204 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.14.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/parser@npm:7.28.3" + dependencies: + "@babel/types": "npm:^7.28.2" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/1f41eb82623b0ca0f94521b57f4790c6c457cd922b8e2597985b36bdec24114a9ccf54640286a760ceb60f11fe9102d192bf60477aee77f5d45f1029b9b72729 + languageName: node + linkType: hard + +"@babel/plugin-syntax-async-generators@npm:^7.8.4": + version: 7.8.4 + resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/d13efb282838481348c71073b6be6245b35d4f2f964a8f71e4174f235009f929ef7613df25f8d2338e2d3e44bc4265a9f8638c6aaa136d7a61fe95985f9725c8 + languageName: node + linkType: hard + +"@babel/plugin-syntax-bigint@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-bigint@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/686891b81af2bc74c39013655da368a480f17dd237bf9fbc32048e5865cb706d5a8f65438030da535b332b1d6b22feba336da8fa931f663b6b34e13147d12dde + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-properties@npm:^7.12.13": + version: 7.12.13 + resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.12.13" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/95168fa186416195280b1264fb18afcdcdcea780b3515537b766cb90de6ce042d42dd6a204a39002f794ae5845b02afb0fd4861a3308a861204a55e68310a120 + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-static-block@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-class-static-block@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/4464bf9115f4a2d02ce1454411baf9cfb665af1da53709c5c56953e5e2913745b0fcce82982a00463d6facbdd93445c691024e310b91431a1e2f024b158f6371 + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-attributes@npm:^7.24.7": + version: 7.27.1 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/e66f7a761b8360419bbb93ab67d87c8a97465ef4637a985ff682ce7ba6918b34b29d81190204cf908d0933058ee7b42737423cd8a999546c21b3aabad4affa9a + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-meta@npm:^7.10.4": + version: 7.10.4 + resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/0b08b5e4c3128523d8e346f8cfc86824f0da2697b1be12d71af50a31aff7a56ceb873ed28779121051475010c28d6146a6bfea8518b150b71eeb4e46190172ee + languageName: node + linkType: hard + +"@babel/plugin-syntax-json-strings@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-json-strings@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/e98f31b2ec406c57757d115aac81d0336e8434101c224edd9a5c93cefa53faf63eacc69f3138960c8b25401315af03df37f68d316c151c4b933136716ed6906e + languageName: node + linkType: hard + +"@babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.27.1 + resolution: "@babel/plugin-syntax-jsx@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/bc5afe6a458d5f0492c02a54ad98c5756a0c13bd6d20609aae65acd560a9e141b0876da5f358dce34ea136f271c1016df58b461184d7ae9c4321e0f98588bc84 + languageName: node + linkType: hard + +"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4": + version: 7.10.4 + resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/2594cfbe29411ad5bc2ad4058de7b2f6a8c5b86eda525a993959438615479e59c012c14aec979e538d60a584a1a799b60d1b8942c3b18468cb9d99b8fd34cd0b + languageName: node + linkType: hard + +"@babel/plugin-syntax-nullish-coalescing-operator@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-nullish-coalescing-operator@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/2024fbb1162899094cfc81152449b12bd0cc7053c6d4bda8ac2852545c87d0a851b1b72ed9560673cbf3ef6248257262c3c04aabf73117215c1b9cc7dd2542ce + languageName: node + linkType: hard + +"@babel/plugin-syntax-numeric-separator@npm:^7.10.4": + version: 7.10.4 + resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/c55a82b3113480942c6aa2fcbe976ff9caa74b7b1109ff4369641dfbc88d1da348aceb3c31b6ed311c84d1e7c479440b961906c735d0ab494f688bf2fd5b9bb9 + languageName: node + linkType: hard + +"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/ee1eab52ea6437e3101a0a7018b0da698545230015fc8ab129d292980ec6dff94d265e9e90070e8ae5fed42f08f1622c14c94552c77bcac784b37f503a82ff26 + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-catch-binding@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-catch-binding@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/27e2493ab67a8ea6d693af1287f7e9acec206d1213ff107a928e85e173741e1d594196f99fec50e9dde404b09164f39dec5864c767212154ffe1caa6af0bc5af + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-chaining@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-chaining@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.8.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/46edddf2faa6ebf94147b8e8540dfc60a5ab718e2de4d01b2c0bdf250a4d642c2bd47cbcbb739febcb2bf75514dbcefad3c52208787994b8d0f8822490f55e81 + languageName: node + linkType: hard + +"@babel/plugin-syntax-private-property-in-object@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-private-property-in-object@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/69822772561706c87f0a65bc92d0772cea74d6bc0911537904a676d5ff496a6d3ac4e05a166d8125fce4a16605bace141afc3611074e170a994e66e5397787f3 + languageName: node + linkType: hard + +"@babel/plugin-syntax-top-level-await@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/14bf6e65d5bc1231ffa9def5f0ef30b19b51c218fcecaa78cd1bdf7939dfdf23f90336080b7f5196916368e399934ce5d581492d8292b46a2fb569d8b2da106f + languageName: node + linkType: hard + +"@babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.27.1 + resolution: "@babel/plugin-syntax-typescript@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/11589b4c89c66ef02d57bf56c6246267851ec0c361f58929327dc3e070b0dab644be625bbe7fb4c4df30c3634bfdfe31244e1f517be397d2def1487dbbe3c37d + languageName: node + linkType: hard + "@babel/plugin-transform-react-jsx-self@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-react-jsx-self@npm:7.27.1" @@ -936,7 +1193,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.25.0, @babel/template@npm:^7.27.2": +"@babel/template@npm:^7.25.0, @babel/template@npm:^7.27.2, @babel/template@npm:^7.3.3": version: 7.27.2 resolution: "@babel/template@npm:7.27.2" dependencies: @@ -977,6 +1234,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/traverse@npm:7.28.3" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.28.3" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.3" + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.28.2" + debug: "npm:^4.3.1" + checksum: 10c0/26e95b29a46925b7b41255e03185b7e65b2c4987e14bbee7bbf95867fb19c69181f301bbe1c7b201d4fe0cce6aa0cbea0282dad74b3a0fef3d9058f6c76fdcb3 + languageName: node + linkType: hard + "@babel/types@npm:7.25.6": version: 7.25.6 resolution: "@babel/types@npm:7.25.6" @@ -988,7 +1260,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.6, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.0, @babel/types@npm:^7.28.2": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.6, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.0, @babel/types@npm:^7.28.2, @babel/types@npm:^7.3.3": version: 7.28.2 resolution: "@babel/types@npm:7.28.2" dependencies: @@ -998,6 +1270,13 @@ __metadata: languageName: node linkType: hard +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 10c0/6b80ae4cb3db53f486da2dc63b6e190a74c8c3cca16bb2733f234a0b6a9382b09b146488ae08e2b22cf00f6c83e20f3e040a2f7894f05c045c946d6a090b1d52 + languageName: node + linkType: hard + "@biomejs/biome@npm:^1.9.4": version: 1.9.4 resolution: "@biomejs/biome@npm:1.9.4" @@ -1938,6 +2217,26 @@ __metadata: languageName: node linkType: hard +"@istanbuljs/load-nyc-config@npm:^1.0.0": + version: 1.1.0 + resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" + dependencies: + camelcase: "npm:^5.3.1" + find-up: "npm:^4.1.0" + get-package-type: "npm:^0.1.0" + js-yaml: "npm:^3.13.1" + resolve-from: "npm:^5.0.0" + checksum: 10c0/dd2a8b094887da5a1a2339543a4933d06db2e63cbbc2e288eb6431bd832065df0c099d091b6a67436e71b7d6bf85f01ce7c15f9253b4cbebcc3b9a496165ba42 + languageName: node + linkType: hard + +"@istanbuljs/schema@npm:^0.1.2, @istanbuljs/schema@npm:^0.1.3": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 10c0/61c5286771676c9ca3eb2bd8a7310a9c063fb6e0e9712225c8471c582d157392c88f5353581c8c9adbe0dff98892317d2fdfc56c3499aa42e0194405206a963a + languageName: node + linkType: hard + "@jercle/yargonaut@npm:1.1.5, @jercle/yargonaut@npm:^1.1.5": version: 1.1.5 resolution: "@jercle/yargonaut@npm:1.1.5" @@ -1949,6 +2248,236 @@ __metadata: languageName: node linkType: hard +"@jest/console@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/console@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10c0/7be408781d0a6f657e969cbec13b540c329671819c2f57acfad0dae9dbfe2c9be859f38fe99b35dba9ff1536937dc6ddc69fdcd2794812fa3c647a1619797f6c + languageName: node + linkType: hard + +"@jest/core@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/core@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/reporters": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-changed-files: "npm:^29.7.0" + jest-config: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-resolve-dependencies: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-ansi: "npm:^6.0.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10c0/934f7bf73190f029ac0f96662c85cd276ec460d407baf6b0dbaec2872e157db4d55a7ee0b1c43b18874602f662b37cb973dda469a4e6d88b4e4845b521adeeb2 + languageName: node + linkType: hard + +"@jest/environment@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/environment@npm:29.7.0" + dependencies: + "@jest/fake-timers": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.7.0" + checksum: 10c0/c7b1b40c618f8baf4d00609022d2afa086d9c6acc706f303a70bb4b67275868f620ad2e1a9efc5edd418906157337cce50589a627a6400bbdf117d351b91ef86 + languageName: node + linkType: hard + +"@jest/expect-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect-utils@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + checksum: 10c0/60b79d23a5358dc50d9510d726443316253ecda3a7fb8072e1526b3e0d3b14f066ee112db95699b7a43ad3f0b61b750c72e28a5a1cac361d7a2bb34747fa938a + languageName: node + linkType: hard + +"@jest/expect@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect@npm:29.7.0" + dependencies: + expect: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + checksum: 10c0/b41f193fb697d3ced134349250aed6ccea075e48c4f803159db102b826a4e473397c68c31118259868fd69a5cba70e97e1c26d2c2ff716ca39dc73a2ccec037e + languageName: node + linkType: hard + +"@jest/fake-timers@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/fake-timers@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@sinonjs/fake-timers": "npm:^10.0.2" + "@types/node": "npm:*" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10c0/cf0a8bcda801b28dc2e2b2ba36302200ee8104a45ad7a21e6c234148932f826cb3bc57c8df3b7b815aeea0861d7b6ca6f0d4778f93b9219398ef28749e03595c + languageName: node + linkType: hard + +"@jest/globals@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/globals@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + jest-mock: "npm:^29.7.0" + checksum: 10c0/a385c99396878fe6e4460c43bd7bb0a5cc52befb462cc6e7f2a3810f9e7bcce7cdeb51908fd530391ee452dc856c98baa2c5f5fa8a5b30b071d31ef7f6955cea + languageName: node + linkType: hard + +"@jest/reporters@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/reporters@npm:29.7.0" + dependencies: + "@bcoe/v8-coverage": "npm:^0.2.3" + "@jest/console": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + collect-v8-coverage: "npm:^1.0.0" + exit: "npm:^0.1.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + istanbul-lib-coverage: "npm:^3.0.0" + istanbul-lib-instrument: "npm:^6.0.0" + istanbul-lib-report: "npm:^3.0.0" + istanbul-lib-source-maps: "npm:^4.0.0" + istanbul-reports: "npm:^3.1.3" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + slash: "npm:^3.0.0" + string-length: "npm:^4.0.1" + strip-ansi: "npm:^6.0.0" + v8-to-istanbul: "npm:^9.0.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10c0/a754402a799541c6e5aff2c8160562525e2a47e7d568f01ebfc4da66522de39cbb809bbb0a841c7052e4270d79214e70aec3c169e4eae42a03bc1a8a20cb9fa2 + languageName: node + linkType: hard + +"@jest/schemas@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/schemas@npm:29.6.3" + dependencies: + "@sinclair/typebox": "npm:^0.27.8" + checksum: 10c0/b329e89cd5f20b9278ae1233df74016ebf7b385e0d14b9f4c1ad18d096c4c19d1e687aa113a9c976b16ec07f021ae53dea811fb8c1248a50ac34fbe009fdf6be + languageName: node + linkType: hard + +"@jest/source-map@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/source-map@npm:29.6.3" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.18" + callsites: "npm:^3.0.0" + graceful-fs: "npm:^4.2.9" + checksum: 10c0/a2f177081830a2e8ad3f2e29e20b63bd40bade294880b595acf2fc09ec74b6a9dd98f126a2baa2bf4941acd89b13a4ade5351b3885c224107083a0059b60a219 + languageName: node + linkType: hard + +"@jest/test-result@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-result@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + collect-v8-coverage: "npm:^1.0.0" + checksum: 10c0/7de54090e54a674ca173470b55dc1afdee994f2d70d185c80236003efd3fa2b753fff51ffcdda8e2890244c411fd2267529d42c4a50a8303755041ee493e6a04 + languageName: node + linkType: hard + +"@jest/test-sequencer@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-sequencer@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + slash: "npm:^3.0.0" + checksum: 10c0/593a8c4272797bb5628984486080cbf57aed09c7cfdc0a634e8c06c38c6bef329c46c0016e84555ee55d1cd1f381518cf1890990ff845524c1123720c8c1481b + languageName: node + linkType: hard + +"@jest/transform@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/transform@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + babel-plugin-istanbul: "npm:^6.1.1" + chalk: "npm:^4.0.0" + convert-source-map: "npm:^2.0.0" + fast-json-stable-stringify: "npm:^2.1.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + pirates: "npm:^4.0.4" + slash: "npm:^3.0.0" + write-file-atomic: "npm:^4.0.2" + checksum: 10c0/7f4a7f73dcf45dfdf280c7aa283cbac7b6e5a904813c3a93ead7e55873761fc20d5c4f0191d2019004fac6f55f061c82eb3249c2901164ad80e362e7a7ede5a6 + languageName: node + linkType: hard + +"@jest/types@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/types@npm:29.6.3" + dependencies: + "@jest/schemas": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + "@types/istanbul-reports": "npm:^3.0.0" + "@types/node": "npm:*" + "@types/yargs": "npm:^17.0.8" + chalk: "npm:^4.0.0" + checksum: 10c0/ea4e493dd3fb47933b8ccab201ae573dcc451f951dc44ed2a86123cd8541b82aa9d2b1031caf9b1080d6673c517e2dcc25a44b2dc4f3fbc37bfc965d444888c0 + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" @@ -1983,7 +2512,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": version: 0.3.30 resolution: "@jridgewell/trace-mapping@npm:0.3.30" dependencies: @@ -2006,9 +2535,12 @@ __metadata: "@medusajs/ui": "npm:^4.0.3" "@swc/core": "npm:1.5.7" "@types/braintree": "npm:^3.3.14" + "@types/jest": "npm:^29.5.12" "@types/jsonwebtoken": "npm:^9.0.10" braintree: "npm:^3.30.0" + jest: "npm:^29.7.0" jsonwebtoken: "npm:^9.0.2" + ts-jest: "npm:^29.2.5" peerDependencies: "@medusajs/admin-sdk": ^2.8.2 "@medusajs/cli": ^2.8.2 @@ -8432,6 +8964,31 @@ __metadata: languageName: node linkType: hard +"@sinclair/typebox@npm:^0.27.8": + version: 0.27.8 + resolution: "@sinclair/typebox@npm:0.27.8" + checksum: 10c0/ef6351ae073c45c2ac89494dbb3e1f87cc60a93ce4cde797b782812b6f97da0d620ae81973f104b43c9b7eaa789ad20ba4f6a1359f1cc62f63729a55a7d22d4e + languageName: node + linkType: hard + +"@sinonjs/commons@npm:^3.0.0": + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" + dependencies: + type-detect: "npm:4.0.8" + checksum: 10c0/1227a7b5bd6c6f9584274db996d7f8cee2c8c350534b9d0141fc662eaf1f292ea0ae3ed19e5e5271c8fd390d27e492ca2803acd31a1978be2cdc6be0da711403 + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^10.0.2": + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" + dependencies: + "@sinonjs/commons": "npm:^3.0.0" + checksum: 10c0/2e2fb6cc57f227912814085b7b01fede050cd4746ea8d49a1e44d5a0e56a804663b0340ae2f11af7559ea9bf4d087a11f2f646197a660ea3cb04e19efc04aa63 + languageName: node + linkType: hard + "@smithy/abort-controller@npm:^4.0.5": version: 4.0.5 resolution: "@smithy/abort-controller@npm:4.0.5" @@ -9302,7 +9859,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.20.5": +"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" dependencies: @@ -9334,7 +9891,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__traverse@npm:*": +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": version: 7.28.0 resolution: "@types/babel__traverse@npm:7.28.0" dependencies: @@ -9437,6 +9994,15 @@ __metadata: languageName: node linkType: hard +"@types/graceful-fs@npm:^4.1.3": + version: 4.1.9 + resolution: "@types/graceful-fs@npm:4.1.9" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/235d2fc69741448e853333b7c3d1180a966dd2b8972c8cbcd6b2a0c6cd7f8d582ab2b8e58219dbc62cce8f1b40aa317ff78ea2201cdd8249da5025adebed6f0b + languageName: node + linkType: hard + "@types/http-errors@npm:*": version: 2.0.5 resolution: "@types/http-errors@npm:2.0.5" @@ -9444,6 +10010,41 @@ __metadata: languageName: node linkType: hard +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": + version: 2.0.6 + resolution: "@types/istanbul-lib-coverage@npm:2.0.6" + checksum: 10c0/3948088654f3eeb45363f1db158354fb013b362dba2a5c2c18c559484d5eb9f6fd85b23d66c0a7c2fcfab7308d0a585b14dadaca6cc8bf89ebfdc7f8f5102fb7 + languageName: node + linkType: hard + +"@types/istanbul-lib-report@npm:*": + version: 3.0.3 + resolution: "@types/istanbul-lib-report@npm:3.0.3" + dependencies: + "@types/istanbul-lib-coverage": "npm:*" + checksum: 10c0/247e477bbc1a77248f3c6de5dadaae85ff86ac2d76c5fc6ab1776f54512a745ff2a5f791d22b942e3990ddbd40f3ef5289317c4fca5741bedfaa4f01df89051c + languageName: node + linkType: hard + +"@types/istanbul-reports@npm:^3.0.0": + version: 3.0.4 + resolution: "@types/istanbul-reports@npm:3.0.4" + dependencies: + "@types/istanbul-lib-report": "npm:*" + checksum: 10c0/1647fd402aced5b6edac87274af14ebd6b3a85447ef9ad11853a70fd92a98d35f81a5d3ea9fcb5dbb5834e800c6e35b64475e33fcae6bfa9acc70d61497c54ee + languageName: node + linkType: hard + +"@types/jest@npm:^29.5.12": + version: 29.5.14 + resolution: "@types/jest@npm:29.5.14" + dependencies: + expect: "npm:^29.0.0" + pretty-format: "npm:^29.0.0" + checksum: 10c0/18e0712d818890db8a8dab3d91e9ea9f7f19e3f83c2e50b312f557017dc81466207a71f3ed79cf4428e813ba939954fa26ffa0a9a7f153181ba174581b1c2aed + languageName: node + linkType: hard + "@types/jsonwebtoken@npm:^9.0.10": version: 9.0.10 resolution: "@types/jsonwebtoken@npm:9.0.10" @@ -9595,6 +10196,13 @@ __metadata: languageName: node linkType: hard +"@types/stack-utils@npm:^2.0.0": + version: 2.0.3 + resolution: "@types/stack-utils@npm:2.0.3" + checksum: 10c0/1f4658385ae936330581bcb8aa3a066df03867d90281cdf89cc356d404bd6579be0f11902304e1f775d92df22c6dd761d4451c804b0a4fba973e06211e9bd77c + languageName: node + linkType: hard + "@types/triple-beam@npm:^1.3.2": version: 1.3.5 resolution: "@types/triple-beam@npm:1.3.5" @@ -9616,6 +10224,22 @@ __metadata: languageName: node linkType: hard +"@types/yargs-parser@npm:*": + version: 21.0.3 + resolution: "@types/yargs-parser@npm:21.0.3" + checksum: 10c0/e71c3bd9d0b73ca82e10bee2064c384ab70f61034bbfb78e74f5206283fc16a6d85267b606b5c22cb2a3338373586786fed595b2009825d6a9115afba36560a0 + languageName: node + linkType: hard + +"@types/yargs@npm:^17.0.8": + version: 17.0.33 + resolution: "@types/yargs@npm:17.0.33" + dependencies: + "@types/yargs-parser": "npm:*" + checksum: 10c0/d16937d7ac30dff697801c3d6f235be2166df42e4a88bf730fa6dc09201de3727c0a9500c59a672122313341de5f24e45ee0ff579c08ce91928e519090b7906b + languageName: node + linkType: hard + "@uiw/react-json-view@npm:^2.0.0-alpha.17": version: 2.0.0-alpha.34 resolution: "@uiw/react-json-view@npm:2.0.0-alpha.34" @@ -9785,6 +10409,13 @@ __metadata: languageName: node linkType: hard +"ansi-styles@npm:^5.0.0": + version: 5.2.0 + resolution: "ansi-styles@npm:5.2.0" + checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df + languageName: node + linkType: hard + "ansi-styles@npm:^6.1.0": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" @@ -9806,7 +10437,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:~3.1.2": +"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -9955,6 +10586,85 @@ __metadata: languageName: node linkType: hard +"babel-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "babel-jest@npm:29.7.0" + dependencies: + "@jest/transform": "npm:^29.7.0" + "@types/babel__core": "npm:^7.1.14" + babel-plugin-istanbul: "npm:^6.1.1" + babel-preset-jest: "npm:^29.6.3" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + slash: "npm:^3.0.0" + peerDependencies: + "@babel/core": ^7.8.0 + checksum: 10c0/2eda9c1391e51936ca573dd1aedfee07b14c59b33dbe16ef347873ddd777bcf6e2fc739681e9e9661ab54ef84a3109a03725be2ac32cd2124c07ea4401cbe8c1 + languageName: node + linkType: hard + +"babel-plugin-istanbul@npm:^6.1.1": + version: 6.1.1 + resolution: "babel-plugin-istanbul@npm:6.1.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.0.0" + "@istanbuljs/load-nyc-config": "npm:^1.0.0" + "@istanbuljs/schema": "npm:^0.1.2" + istanbul-lib-instrument: "npm:^5.0.4" + test-exclude: "npm:^6.0.0" + checksum: 10c0/1075657feb705e00fd9463b329921856d3775d9867c5054b449317d39153f8fbcebd3e02ebf00432824e647faff3683a9ca0a941325ef1afe9b3c4dd51b24beb + languageName: node + linkType: hard + +"babel-plugin-jest-hoist@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-plugin-jest-hoist@npm:29.6.3" + dependencies: + "@babel/template": "npm:^7.3.3" + "@babel/types": "npm:^7.3.3" + "@types/babel__core": "npm:^7.1.14" + "@types/babel__traverse": "npm:^7.0.6" + checksum: 10c0/7e6451caaf7dce33d010b8aafb970e62f1b0c0b57f4978c37b0d457bbcf0874d75a395a102daf0bae0bd14eafb9f6e9a165ee5e899c0a4f1f3bb2e07b304ed2e + languageName: node + linkType: hard + +"babel-preset-current-node-syntax@npm:^1.0.0": + version: 1.2.0 + resolution: "babel-preset-current-node-syntax@npm:1.2.0" + dependencies: + "@babel/plugin-syntax-async-generators": "npm:^7.8.4" + "@babel/plugin-syntax-bigint": "npm:^7.8.3" + "@babel/plugin-syntax-class-properties": "npm:^7.12.13" + "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" + "@babel/plugin-syntax-import-attributes": "npm:^7.24.7" + "@babel/plugin-syntax-import-meta": "npm:^7.10.4" + "@babel/plugin-syntax-json-strings": "npm:^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" + "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" + "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" + "@babel/plugin-syntax-top-level-await": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0 || ^8.0.0-0 + checksum: 10c0/94a4f81cddf9b051045d08489e4fff7336292016301664c138cfa3d9ffe3fe2ba10a24ad6ae589fd95af1ac72ba0216e1653555c187e694d7b17be0c002bea10 + languageName: node + linkType: hard + +"babel-preset-jest@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-preset-jest@npm:29.6.3" + dependencies: + babel-plugin-jest-hoist: "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/ec5fd0276b5630b05f0c14bb97cc3815c6b31600c683ebb51372e54dcb776cff790bdeeabd5b8d01ede375a040337ccbf6a3ccd68d3a34219125945e167ad943 + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -10099,6 +10809,15 @@ __metadata: languageName: node linkType: hard +"bs-logger@npm:^0.2.6": + version: 0.2.6 + resolution: "bs-logger@npm:0.2.6" + dependencies: + fast-json-stable-stringify: "npm:2.x" + checksum: 10c0/80e89aaaed4b68e3374ce936f2eb097456a0dddbf11f75238dbd53140b1e39259f0d248a5089ed456f1158984f22191c3658d54a713982f676709fbe1a6fa5a0 + languageName: node + linkType: hard + "bser@npm:2.1.1": version: 2.1.1 resolution: "bser@npm:2.1.1" @@ -10228,6 +10947,13 @@ __metadata: languageName: node linkType: hard +"callsites@npm:^3.0.0": + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301 + languageName: node + linkType: hard + "camel-case@npm:^4.1.2": version: 4.1.2 resolution: "camel-case@npm:4.1.2" @@ -10245,7 +10971,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.0.0": +"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: 10c0/92ff9b443bfe8abb15f2b1513ca182d16126359ad4f955ebc83dc4ddcc4ef3fdd2c078bc223f2673dc223488e75c99b16cc4d056624374b799e6a1555cf61b23 @@ -10337,6 +11063,13 @@ __metadata: languageName: node linkType: hard +"char-regex@npm:^1.0.2": + version: 1.0.2 + resolution: "char-regex@npm:1.0.2" + checksum: 10c0/57a09a86371331e0be35d9083ba429e86c4f4648ecbe27455dbfb343037c16ee6fdc7f6b61f433a57cc5ded5561d71c56a150e018f40c2ffb7bc93a26dae341e + languageName: node + linkType: hard + "chardet@npm:^2.1.0": version: 2.1.0 resolution: "chardet@npm:2.1.0" @@ -10405,6 +11138,13 @@ __metadata: languageName: node linkType: hard +"cjs-module-lexer@npm:^1.0.0": + version: 1.4.3 + resolution: "cjs-module-lexer@npm:1.4.3" + checksum: 10c0/076b3af85adc4d65dbdab1b5b240fe5b45d44fcf0ef9d429044dd94d19be5589376805c44fb2d4b3e684e5fe6a9b7cf3e426476a6507c45283c5fc6ff95240be + languageName: node + linkType: hard + "clean-stack@npm:^3.0.0": version: 3.0.1 resolution: "clean-stack@npm:3.0.1" @@ -10574,6 +11314,20 @@ __metadata: languageName: node linkType: hard +"co@npm:^4.6.0": + version: 4.6.0 + resolution: "co@npm:4.6.0" + checksum: 10c0/c0e85ea0ca8bf0a50cbdca82efc5af0301240ca88ebe3644a6ffb8ffe911f34d40f8fbcf8f1d52c5ddd66706abd4d3bfcd64259f1e8e2371d4f47573b0dc8c28 + languageName: node + linkType: hard + +"collect-v8-coverage@npm:^1.0.0": + version: 1.0.2 + resolution: "collect-v8-coverage@npm:1.0.2" + checksum: 10c0/ed7008e2e8b6852c5483b444a3ae6e976e088d4335a85aa0a9db2861c5f1d31bd2d7ff97a60469b3388deeba661a619753afbe201279fb159b4b9548ab8269a1 + languageName: node + linkType: hard + "color-convert@npm:^1.9.3": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -10871,6 +11625,23 @@ __metadata: languageName: node linkType: hard +"create-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "create-jest@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + prompts: "npm:^2.0.1" + bin: + create-jest: bin/create-jest.js + checksum: 10c0/e7e54c280692470d3398f62a6238fd396327e01c6a0757002833f06d00afc62dd7bfe04ff2b9cd145264460e6b4d1eb8386f2925b7e567f97939843b7b0e812f + languageName: node + linkType: hard + "create-require@npm:^1.1.0": version: 1.1.1 resolution: "create-require@npm:1.1.1" @@ -11037,6 +11808,18 @@ __metadata: languageName: node linkType: hard +"dedent@npm:^1.0.0": + version: 1.6.0 + resolution: "dedent@npm:1.6.0" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: 10c0/671b8f5e390dd2a560862c4511dd6d2638e71911486f78cb32116551f8f2aa6fcaf50579ffffb2f866d46b5b80fd72470659ca5760ede8f967619ef7df79e8a5 + languageName: node + linkType: hard + "deeks@npm:3.1.0": version: 3.1.0 resolution: "deeks@npm:3.1.0" @@ -11109,6 +11892,13 @@ __metadata: languageName: node linkType: hard +"detect-newline@npm:^3.0.0": + version: 3.1.0 + resolution: "detect-newline@npm:3.1.0" + checksum: 10c0/c38cfc8eeb9fda09febb44bcd85e467c970d4e3bf526095394e5a4f18bc26dd0cf6b22c69c1fa9969261521c593836db335c2795218f6d781a512aea2fb8209d + languageName: node + linkType: hard + "detect-node-es@npm:^1.1.0": version: 1.1.0 resolution: "detect-node-es@npm:1.1.0" @@ -11123,6 +11913,13 @@ __metadata: languageName: node linkType: hard +"diff-sequences@npm:^29.6.3": + version: 29.6.3 + resolution: "diff-sequences@npm:29.6.3" + checksum: 10c0/32e27ac7dbffdf2fb0eb5a84efd98a9ad084fbabd5ac9abb8757c6770d5320d2acd172830b28c4add29bb873d59420601dfc805ac4064330ce59b1adfd0593b2 + languageName: node + linkType: hard + "diff@npm:^4.0.1": version: 4.0.2 resolution: "diff@npm:4.0.2" @@ -11250,7 +12047,7 @@ __metadata: languageName: node linkType: hard -"emittery@npm:^0.13.0": +"emittery@npm:^0.13.0, emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" checksum: 10c0/1573d0ae29ab34661b6c63251ff8f5facd24ccf6a823f19417ae8ba8c88ea450325788c67f16c99edec8de4b52ce93a10fe441ece389fd156e88ee7dab9bfa35 @@ -11315,6 +12112,15 @@ __metadata: languageName: node linkType: hard +"error-ex@npm:^1.3.1": + version: 1.3.2 + resolution: "error-ex@npm:1.3.2" + dependencies: + is-arrayish: "npm:^0.2.1" + checksum: 10c0/ba827f89369b4c93382cfca5a264d059dfefdaa56ecc5e338ffa58a6471f5ed93b71a20add1d52290a4873d92381174382658c885ac1a2305f7baca363ce9cce + languageName: node + linkType: hard + "es-define-property@npm:^1.0.1": version: 1.0.1 resolution: "es-define-property@npm:1.0.1" @@ -11547,6 +12353,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:^2.0.0": + version: 2.0.0 + resolution: "escape-string-regexp@npm:2.0.0" + checksum: 10c0/2530479fe8db57eace5e8646c9c2a9c80fa279614986d16dcc6bcaceb63ae77f05a851ba6c43756d816c61d7f4534baf56e3c705e3e0d884818a46808811c507 + languageName: node + linkType: hard + "esm@npm:^3.2.25": version: 3.2.25 resolution: "esm@npm:3.2.25" @@ -11571,7 +12384,7 @@ __metadata: languageName: node linkType: hard -"execa@npm:^5.1.1": +"execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -11588,6 +12401,26 @@ __metadata: languageName: node linkType: hard +"exit@npm:^0.1.2": + version: 0.1.2 + resolution: "exit@npm:0.1.2" + checksum: 10c0/71d2ad9b36bc25bb8b104b17e830b40a08989be7f7d100b13269aaae7c3784c3e6e1e88a797e9e87523993a25ba27c8958959a554535370672cfb4d824af8989 + languageName: node + linkType: hard + +"expect@npm:^29.0.0, expect@npm:^29.7.0": + version: 29.7.0 + resolution: "expect@npm:29.7.0" + dependencies: + "@jest/expect-utils": "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10c0/2eddeace66e68b8d8ee5f7be57f3014b19770caaf6815c7a08d131821da527fb8c8cb7b3dcd7c883d2d3d8d184206a4268984618032d1e4b16dc8d6596475d41 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.2 resolution: "exponential-backoff@npm:3.1.2" @@ -11677,6 +12510,13 @@ __metadata: languageName: node linkType: hard +"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.1.0": + version: 2.1.0 + resolution: "fast-json-stable-stringify@npm:2.1.0" + checksum: 10c0/7f081eb0b8a64e0057b3bb03f974b3ef00135fbf36c1c710895cd9300f13c94ba809bb3a81cf4e1b03f6e5285610a61abbd7602d0652de423144dfee5a389c9b + languageName: node + linkType: hard + "fast-uri@npm:^3.0.1": version: 3.0.6 resolution: "fast-uri@npm:3.0.6" @@ -11815,7 +12655,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^4.1.0": +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" dependencies: @@ -12132,7 +12972,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.2.3, glob@npm:^7.1.4, glob@npm:^7.1.6": +"glob@npm:7.2.3, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -12200,7 +13040,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -12225,6 +13065,24 @@ __metadata: languageName: node linkType: hard +"handlebars@npm:^4.7.8": + version: 4.7.8 + resolution: "handlebars@npm:4.7.8" + dependencies: + minimist: "npm:^1.2.5" + neo-async: "npm:^2.6.2" + source-map: "npm:^0.6.1" + uglify-js: "npm:^3.1.4" + wordwrap: "npm:^1.0.0" + dependenciesMeta: + uglify-js: + optional: true + bin: + handlebars: bin/handlebars + checksum: 10c0/7aff423ea38a14bb379316f3857fe0df3c5d66119270944247f155ba1f08e07a92b340c58edaa00cfe985c21508870ee5183e0634dcb53dd405f35c93ef7f10d + languageName: node + linkType: hard + "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -12276,6 +13134,13 @@ __metadata: languageName: node linkType: hard +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0 + languageName: node + linkType: hard + "html-parse-stringify@npm:^3.0.1": version: 3.0.1 resolution: "html-parse-stringify@npm:3.0.1" @@ -12428,6 +13293,18 @@ __metadata: languageName: node linkType: hard +"import-local@npm:^3.0.2": + version: 3.2.0 + resolution: "import-local@npm:3.2.0" + dependencies: + pkg-dir: "npm:^4.2.0" + resolve-cwd: "npm:^3.0.0" + bin: + import-local-fixture: fixtures/cli.js + checksum: 10c0/94cd6367a672b7e0cb026970c85b76902d2710a64896fa6de93bd5c571dd03b228c5759308959de205083e3b1c61e799f019c9e36ee8e9c523b993e1057f0433 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -12558,6 +13435,13 @@ __metadata: languageName: node linkType: hard +"is-arrayish@npm:^0.2.1": + version: 0.2.1 + resolution: "is-arrayish@npm:0.2.1" + checksum: 10c0/e7fb686a739068bb70f860b39b67afc62acc62e36bb61c5f965768abce1873b379c563e61dd2adad96ebb7edf6651111b385e490cf508378959b0ed4cac4e729 + languageName: node + linkType: hard + "is-arrayish@npm:^0.3.1": version: 0.3.2 resolution: "is-arrayish@npm:0.3.2" @@ -12613,6 +13497,13 @@ __metadata: languageName: node linkType: hard +"is-generator-fn@npm:^2.0.0": + version: 2.1.0 + resolution: "is-generator-fn@npm:2.1.0" + checksum: 10c0/2957cab387997a466cd0bf5c1b6047bd21ecb32bdcfd8996b15747aa01002c1c88731802f1b3d34ac99f4f6874b626418bd118658cf39380fe5fff32a3af9c4d + languageName: node + linkType: hard + "is-glob@npm:^2.0.0": version: 2.0.1 resolution: "is-glob@npm:2.0.1" @@ -12686,117 +13577,621 @@ __metadata: languageName: node linkType: hard -"is-retry-allowed@npm:^2.2.0": - version: 2.2.0 - resolution: "is-retry-allowed@npm:2.2.0" - checksum: 10c0/013be4f8a0a06a49ed1fe495242952e898325d496202a018f6f9fb3fb9ac8fe3b957a9bd62463d68299ae35dbbda680473c85a9bcefca731b49d500d3ccc08ff +"is-retry-allowed@npm:^2.2.0": + version: 2.2.0 + resolution: "is-retry-allowed@npm:2.2.0" + checksum: 10c0/013be4f8a0a06a49ed1fe495242952e898325d496202a018f6f9fb3fb9ac8fe3b957a9bd62463d68299ae35dbbda680473c85a9bcefca731b49d500d3ccc08ff + languageName: node + linkType: hard + +"is-stream@npm:^2.0.0": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5 + languageName: node + linkType: hard + +"is-typedarray@npm:^1.0.0": + version: 1.0.0 + resolution: "is-typedarray@npm:1.0.0" + checksum: 10c0/4c096275ba041a17a13cca33ac21c16bc4fd2d7d7eb94525e7cd2c2f2c1a3ab956e37622290642501ff4310601e413b675cf399ad6db49855527d2163b3eeeec + languageName: node + linkType: hard + +"is-unc-path@npm:^1.0.0": + version: 1.0.0 + resolution: "is-unc-path@npm:1.0.0" + dependencies: + unc-path-regex: "npm:^0.1.2" + checksum: 10c0/ac1b78f9b748196e3be3d0e722cd4b0f98639247a130a8f2473a58b29baf63fdb1b1c5a12c830660c5ee6ef0279c5418ca8e346f98cbe1a29e433d7ae531d42e + languageName: node + linkType: hard + +"is-unicode-supported@npm:^0.1.0": + version: 0.1.0 + resolution: "is-unicode-supported@npm:0.1.0" + checksum: 10c0/00cbe3455c3756be68d2542c416cab888aebd5012781d6819749fefb15162ff23e38501fe681b3d751c73e8ff561ac09a5293eba6f58fdf0178462ce6dcb3453 + languageName: node + linkType: hard + +"is-upper-case@npm:^2.0.2": + version: 2.0.2 + resolution: "is-upper-case@npm:2.0.2" + dependencies: + tslib: "npm:^2.0.3" + checksum: 10c0/2236f416484a2643d55a07cc95443cecf96cbc5fb0de7f24c506a8bc5cc4c4de885ab56c5ec946eadd95b3b7960bff7ed51cc88511fa8e8a9d92f2f8969622d9 + languageName: node + linkType: hard + +"is-valid-path@npm:^0.1.1": + version: 0.1.1 + resolution: "is-valid-path@npm:0.1.1" + dependencies: + is-invalid-path: "npm:^0.1.0" + checksum: 10c0/05c3533b8d98ac469bec9849e6ee73a07e1f9857e2043c75a9a45d21bae5e11fafb625808d7bd1aaf5cc63e842876c636f9888388a959ee9c33975c7b603c6ba + languageName: node + linkType: hard + +"is-windows@npm:^1.0.1": + version: 1.0.2 + resolution: "is-windows@npm:1.0.2" + checksum: 10c0/b32f418ab3385604a66f1b7a3ce39d25e8881dee0bd30816dc8344ef6ff9df473a732bcc1ec4e84fe99b2f229ae474f7133e8e93f9241686cfcf7eebe53ba7a5 + languageName: node + linkType: hard + +"is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0": + version: 2.2.0 + resolution: "is-wsl@npm:2.2.0" + dependencies: + is-docker: "npm:^2.0.0" + checksum: 10c0/a6fa2d370d21be487c0165c7a440d567274fbba1a817f2f0bfa41cc5e3af25041d84267baa22df66696956038a43973e72fca117918c91431920bdef490fa25e + languageName: node + linkType: hard + +"isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 10c0/18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^5.0.4": + version: 5.2.1 + resolution: "istanbul-lib-instrument@npm:5.2.1" + dependencies: + "@babel/core": "npm:^7.12.3" + "@babel/parser": "npm:^7.14.7" + "@istanbuljs/schema": "npm:^0.1.2" + istanbul-lib-coverage: "npm:^3.2.0" + semver: "npm:^6.3.0" + checksum: 10c0/8a1bdf3e377dcc0d33ec32fe2b6ecacdb1e4358fd0eb923d4326bb11c67622c0ceb99600a680f3dad5d29c66fc1991306081e339b4d43d0b8a2ab2e1d910a6ee + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^6.0.0": + version: 6.0.3 + resolution: "istanbul-lib-instrument@npm:6.0.3" + dependencies: + "@babel/core": "npm:^7.23.9" + "@babel/parser": "npm:^7.23.9" + "@istanbuljs/schema": "npm:^0.1.3" + istanbul-lib-coverage: "npm:^3.2.0" + semver: "npm:^7.5.4" + checksum: 10c0/a1894e060dd2a3b9f046ffdc87b44c00a35516f5e6b7baf4910369acca79e506fc5323a816f811ae23d82334b38e3ddeb8b3b331bd2c860540793b59a8689128 + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: "npm:^3.0.0" + make-dir: "npm:^4.0.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/84323afb14392de8b6a5714bd7e9af845cfbd56cfe71ed276cda2f5f1201aea673c7111901227ee33e68e4364e288d73861eb2ed48f6679d1e69a43b6d9b3ba7 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^4.0.0": + version: 4.0.1 + resolution: "istanbul-lib-source-maps@npm:4.0.1" + dependencies: + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + source-map: "npm:^0.6.1" + checksum: 10c0/19e4cc405016f2c906dff271a76715b3e881fa9faeb3f09a86cb99b8512b3a5ed19cadfe0b54c17ca0e54c1142c9c6de9330d65506e35873994e06634eebeb66 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.3": + version: 3.1.7 + resolution: "istanbul-reports@npm:3.1.7" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10c0/a379fadf9cf8dc5dfe25568115721d4a7eb82fbd50b005a6672aff9c6989b20cc9312d7865814e0859cd8df58cbf664482e1d3604be0afde1f7fc3ccc1394a51 + languageName: node + linkType: hard + +"jackspeak@npm:^3.1.2": + version: 3.4.3 + resolution: "jackspeak@npm:3.4.3" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 10c0/6acc10d139eaefdbe04d2f679e6191b3abf073f111edf10b1de5302c97ec93fffeb2fdd8681ed17f16268aa9dd4f8c588ed9d1d3bffbbfa6e8bf897cbb3149b9 + languageName: node + linkType: hard + +"jackspeak@npm:^4.1.1": + version: 4.1.1 + resolution: "jackspeak@npm:4.1.1" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + checksum: 10c0/84ec4f8e21d6514db24737d9caf65361511f75e5e424980eebca4199f400874f45e562ac20fa8aeb1dd20ca2f3f81f0788b6e9c3e64d216a5794fd6f30e0e042 + languageName: node + linkType: hard + +"jest-changed-files@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-changed-files@npm:29.7.0" + dependencies: + execa: "npm:^5.0.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + checksum: 10c0/e071384d9e2f6bb462231ac53f29bff86f0e12394c1b49ccafbad225ce2ab7da226279a8a94f421949920bef9be7ef574fd86aee22e8adfa149be73554ab828b + languageName: node + linkType: hard + +"jest-circus@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-circus@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/expect": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + co: "npm:^4.6.0" + dedent: "npm:^1.0.0" + is-generator-fn: "npm:^2.0.0" + jest-each: "npm:^29.7.0" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + pretty-format: "npm:^29.7.0" + pure-rand: "npm:^6.0.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10c0/8d15344cf7a9f14e926f0deed64ed190c7a4fa1ed1acfcd81e4cc094d3cc5bf7902ebb7b874edc98ada4185688f90c91e1747e0dfd7ac12463b097968ae74b5e + languageName: node + linkType: hard + +"jest-cli@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-cli@npm:29.7.0" + dependencies: + "@jest/core": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + create-jest: "npm:^29.7.0" + exit: "npm:^0.1.2" + import-local: "npm:^3.0.2" + jest-config: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + yargs: "npm:^17.3.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10c0/a658fd55050d4075d65c1066364595962ead7661711495cfa1dfeecf3d6d0a8ffec532f3dbd8afbb3e172dd5fd2fb2e813c5e10256e7cf2fea766314942fb43a + languageName: node + linkType: hard + +"jest-config@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-config@npm:29.7.0" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/test-sequencer": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-jest: "npm:^29.7.0" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + deepmerge: "npm:^4.2.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-circus: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-runner: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + parse-json: "npm:^5.2.0" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-json-comments: "npm:^3.1.1" + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + ts-node: + optional: true + checksum: 10c0/bab23c2eda1fff06e0d104b00d6adfb1d1aabb7128441899c9bff2247bd26710b050a5364281ce8d52b46b499153bf7e3ee88b19831a8f3451f1477a0246a0f1 + languageName: node + linkType: hard + +"jest-diff@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-diff@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + diff-sequences: "npm:^29.6.3" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/89a4a7f182590f56f526443dde69acefb1f2f0c9e59253c61d319569856c4931eae66b8a3790c443f529267a0ddba5ba80431c585deed81827032b2b2a1fc999 + languageName: node + linkType: hard + +"jest-docblock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-docblock@npm:29.7.0" + dependencies: + detect-newline: "npm:^3.0.0" + checksum: 10c0/d932a8272345cf6b6142bb70a2bb63e0856cc0093f082821577ea5bdf4643916a98744dfc992189d2b1417c38a11fa42466f6111526bc1fb81366f56410f3be9 + languageName: node + linkType: hard + +"jest-each@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-each@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + pretty-format: "npm:^29.7.0" + checksum: 10c0/f7f9a90ebee80cc688e825feceb2613627826ac41ea76a366fa58e669c3b2403d364c7c0a74d862d469b103c843154f8456d3b1c02b487509a12afa8b59edbb4 + languageName: node + linkType: hard + +"jest-environment-node@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-environment-node@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + checksum: 10c0/61f04fec077f8b1b5c1a633e3612fc0c9aa79a0ab7b05600683428f1e01a4d35346c474bde6f439f9fcc1a4aa9a2861ff852d079a43ab64b02105d1004b2592b + languageName: node + linkType: hard + +"jest-get-type@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-get-type@npm:29.6.3" + checksum: 10c0/552e7a97a983d3c2d4e412a44eb7de0430ff773dd99f7500962c268d6dfbfa431d7d08f919c9d960530e5f7f78eb47f267ad9b318265e5092b3ff9ede0db7c2b + languageName: node + linkType: hard + +"jest-haste-map@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-haste-map@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/graceful-fs": "npm:^4.1.3" + "@types/node": "npm:*" + anymatch: "npm:^3.0.3" + fb-watchman: "npm:^2.0.0" + fsevents: "npm:^2.3.2" + graceful-fs: "npm:^4.2.9" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + micromatch: "npm:^4.0.4" + walker: "npm:^1.0.8" + dependenciesMeta: + fsevents: + optional: true + checksum: 10c0/2683a8f29793c75a4728787662972fedd9267704c8f7ef9d84f2beed9a977f1cf5e998c07b6f36ba5603f53cb010c911fe8cd0ac9886e073fe28ca66beefd30c + languageName: node + linkType: hard + +"jest-leak-detector@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-leak-detector@npm:29.7.0" + dependencies: + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/71bb9f77fc489acb842a5c7be030f2b9acb18574dc9fb98b3100fc57d422b1abc55f08040884bd6e6dbf455047a62f7eaff12aa4058f7cbdc11558718ca6a395 languageName: node linkType: hard -"is-stream@npm:^2.0.0": - version: 2.0.1 - resolution: "is-stream@npm:2.0.1" - checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5 +"jest-matcher-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-matcher-utils@npm:29.7.0" + dependencies: + chalk: "npm:^4.0.0" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10c0/0d0e70b28fa5c7d4dce701dc1f46ae0922102aadc24ed45d594dd9b7ae0a8a6ef8b216718d1ab79e451291217e05d4d49a82666e1a3cc2b428b75cd9c933244e languageName: node linkType: hard -"is-typedarray@npm:^1.0.0": - version: 1.0.0 - resolution: "is-typedarray@npm:1.0.0" - checksum: 10c0/4c096275ba041a17a13cca33ac21c16bc4fd2d7d7eb94525e7cd2c2f2c1a3ab956e37622290642501ff4310601e413b675cf399ad6db49855527d2163b3eeeec +"jest-message-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-message-util@npm:29.7.0" + dependencies: + "@babel/code-frame": "npm:^7.12.13" + "@jest/types": "npm:^29.6.3" + "@types/stack-utils": "npm:^2.0.0" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.7.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10c0/850ae35477f59f3e6f27efac5215f706296e2104af39232bb14e5403e067992afb5c015e87a9243ec4d9df38525ef1ca663af9f2f4766aa116f127247008bd22 languageName: node linkType: hard -"is-unc-path@npm:^1.0.0": - version: 1.0.0 - resolution: "is-unc-path@npm:1.0.0" +"jest-mock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-mock@npm:29.7.0" dependencies: - unc-path-regex: "npm:^0.1.2" - checksum: 10c0/ac1b78f9b748196e3be3d0e722cd4b0f98639247a130a8f2473a58b29baf63fdb1b1c5a12c830660c5ee6ef0279c5418ca8e346f98cbe1a29e433d7ae531d42e + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-util: "npm:^29.7.0" + checksum: 10c0/7b9f8349ee87695a309fe15c46a74ab04c853369e5c40952d68061d9dc3159a0f0ed73e215f81b07ee97a9faaf10aebe5877a9d6255068a0977eae6a9ff1d5ac languageName: node linkType: hard -"is-unicode-supported@npm:^0.1.0": - version: 0.1.0 - resolution: "is-unicode-supported@npm:0.1.0" - checksum: 10c0/00cbe3455c3756be68d2542c416cab888aebd5012781d6819749fefb15162ff23e38501fe681b3d751c73e8ff561ac09a5293eba6f58fdf0178462ce6dcb3453 +"jest-pnp-resolver@npm:^1.2.2": + version: 1.2.3 + resolution: "jest-pnp-resolver@npm:1.2.3" + peerDependencies: + jest-resolve: "*" + peerDependenciesMeta: + jest-resolve: + optional: true + checksum: 10c0/86eec0c78449a2de733a6d3e316d49461af6a858070e113c97f75fb742a48c2396ea94150cbca44159ffd4a959f743a47a8b37a792ef6fdad2cf0a5cba973fac languageName: node linkType: hard -"is-upper-case@npm:^2.0.2": - version: 2.0.2 - resolution: "is-upper-case@npm:2.0.2" +"jest-regex-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-regex-util@npm:29.6.3" + checksum: 10c0/4e33fb16c4f42111159cafe26397118dcfc4cf08bc178a67149fb05f45546a91928b820894572679d62559839d0992e21080a1527faad65daaae8743a5705a3b + languageName: node + linkType: hard + +"jest-resolve-dependencies@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve-dependencies@npm:29.7.0" dependencies: - tslib: "npm:^2.0.3" - checksum: 10c0/2236f416484a2643d55a07cc95443cecf96cbc5fb0de7f24c506a8bc5cc4c4de885ab56c5ec946eadd95b3b7960bff7ed51cc88511fa8e8a9d92f2f8969622d9 + jest-regex-util: "npm:^29.6.3" + jest-snapshot: "npm:^29.7.0" + checksum: 10c0/b6e9ad8ae5b6049474118ea6441dfddd385b6d1fc471db0136f7c8fbcfe97137a9665e4f837a9f49f15a29a1deb95a14439b7aec812f3f99d08f228464930f0d languageName: node linkType: hard -"is-valid-path@npm:^0.1.1": - version: 0.1.1 - resolution: "is-valid-path@npm:0.1.1" +"jest-resolve@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve@npm:29.7.0" dependencies: - is-invalid-path: "npm:^0.1.0" - checksum: 10c0/05c3533b8d98ac469bec9849e6ee73a07e1f9857e2043c75a9a45d21bae5e11fafb625808d7bd1aaf5cc63e842876c636f9888388a959ee9c33975c7b603c6ba + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-pnp-resolver: "npm:^1.2.2" + jest-util: "npm:^29.7.0" + jest-validate: "npm:^29.7.0" + resolve: "npm:^1.20.0" + resolve.exports: "npm:^2.0.0" + slash: "npm:^3.0.0" + checksum: 10c0/59da5c9c5b50563e959a45e09e2eace783d7f9ac0b5dcc6375dea4c0db938d2ebda97124c8161310082760e8ebbeff9f6b177c15ca2f57fb424f637a5d2adb47 languageName: node linkType: hard -"is-windows@npm:^1.0.1": - version: 1.0.2 - resolution: "is-windows@npm:1.0.2" - checksum: 10c0/b32f418ab3385604a66f1b7a3ce39d25e8881dee0bd30816dc8344ef6ff9df473a732bcc1ec4e84fe99b2f229ae474f7133e8e93f9241686cfcf7eebe53ba7a5 +"jest-runner@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runner@npm:29.7.0" + dependencies: + "@jest/console": "npm:^29.7.0" + "@jest/environment": "npm:^29.7.0" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + graceful-fs: "npm:^4.2.9" + jest-docblock: "npm:^29.7.0" + jest-environment-node: "npm:^29.7.0" + jest-haste-map: "npm:^29.7.0" + jest-leak-detector: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-resolve: "npm:^29.7.0" + jest-runtime: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + jest-watcher: "npm:^29.7.0" + jest-worker: "npm:^29.7.0" + p-limit: "npm:^3.1.0" + source-map-support: "npm:0.5.13" + checksum: 10c0/2194b4531068d939f14c8d3274fe5938b77fa73126aedf9c09ec9dec57d13f22c72a3b5af01ac04f5c1cf2e28d0ac0b4a54212a61b05f10b5d6b47f2a1097bb4 + languageName: node + linkType: hard + +"jest-runtime@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runtime@npm:29.7.0" + dependencies: + "@jest/environment": "npm:^29.7.0" + "@jest/fake-timers": "npm:^29.7.0" + "@jest/globals": "npm:^29.7.0" + "@jest/source-map": "npm:^29.6.3" + "@jest/test-result": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + cjs-module-lexer: "npm:^1.0.0" + collect-v8-coverage: "npm:^1.0.0" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-mock: "npm:^29.7.0" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.7.0" + jest-snapshot: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + slash: "npm:^3.0.0" + strip-bom: "npm:^4.0.0" + checksum: 10c0/7cd89a1deda0bda7d0941835434e44f9d6b7bd50b5c5d9b0fc9a6c990b2d4d2cab59685ab3cb2850ed4cc37059f6de903af5a50565d7f7f1192a77d3fd6dd2a6 languageName: node linkType: hard -"is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0": - version: 2.2.0 - resolution: "is-wsl@npm:2.2.0" +"jest-snapshot@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-snapshot@npm:29.7.0" dependencies: - is-docker: "npm:^2.0.0" - checksum: 10c0/a6fa2d370d21be487c0165c7a440d567274fbba1a817f2f0bfa41cc5e3af25041d84267baa22df66696956038a43973e72fca117918c91431920bdef490fa25e + "@babel/core": "npm:^7.11.6" + "@babel/generator": "npm:^7.7.2" + "@babel/plugin-syntax-jsx": "npm:^7.7.2" + "@babel/plugin-syntax-typescript": "npm:^7.7.2" + "@babel/types": "npm:^7.3.3" + "@jest/expect-utils": "npm:^29.7.0" + "@jest/transform": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + chalk: "npm:^4.0.0" + expect: "npm:^29.7.0" + graceful-fs: "npm:^4.2.9" + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.7.0" + jest-message-util: "npm:^29.7.0" + jest-util: "npm:^29.7.0" + natural-compare: "npm:^1.4.0" + pretty-format: "npm:^29.7.0" + semver: "npm:^7.5.3" + checksum: 10c0/6e9003c94ec58172b4a62864a91c0146513207bedf4e0a06e1e2ac70a4484088a2683e3a0538d8ea913bcfd53dc54a9b98a98cdfa562e7fe1d1339aeae1da570 languageName: node linkType: hard -"isarray@npm:~1.0.0": - version: 1.0.0 - resolution: "isarray@npm:1.0.0" - checksum: 10c0/18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d +"jest-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-util@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + graceful-fs: "npm:^4.2.9" + picomatch: "npm:^2.2.3" + checksum: 10c0/bc55a8f49fdbb8f51baf31d2a4f312fb66c9db1483b82f602c9c990e659cdd7ec529c8e916d5a89452ecbcfae4949b21b40a7a59d4ffc0cd813a973ab08c8150 languageName: node linkType: hard -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d +"jest-validate@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-validate@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + camelcase: "npm:^6.2.0" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + leven: "npm:^3.1.0" + pretty-format: "npm:^29.7.0" + checksum: 10c0/a20b930480c1ed68778c739f4739dce39423131bc070cd2505ddede762a5570a256212e9c2401b7ae9ba4d7b7c0803f03c5b8f1561c62348213aba18d9dbece2 languageName: node linkType: hard -"isexe@npm:^3.1.1": - version: 3.1.1 - resolution: "isexe@npm:3.1.1" - checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 +"jest-watcher@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-watcher@npm:29.7.0" + dependencies: + "@jest/test-result": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + jest-util: "npm:^29.7.0" + string-length: "npm:^4.0.1" + checksum: 10c0/ec6c75030562fc8f8c727cb8f3b94e75d831fc718785abfc196e1f2a2ebc9a2e38744a15147170039628a853d77a3b695561ce850375ede3a4ee6037a2574567 languageName: node linkType: hard -"jackspeak@npm:^3.1.2": - version: 3.4.3 - resolution: "jackspeak@npm:3.4.3" +"jest-worker@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-worker@npm:29.7.0" dependencies: - "@isaacs/cliui": "npm:^8.0.2" - "@pkgjs/parseargs": "npm:^0.11.0" - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: 10c0/6acc10d139eaefdbe04d2f679e6191b3abf073f111edf10b1de5302c97ec93fffeb2fdd8681ed17f16268aa9dd4f8c588ed9d1d3bffbbfa6e8bf897cbb3149b9 + "@types/node": "npm:*" + jest-util: "npm:^29.7.0" + merge-stream: "npm:^2.0.0" + supports-color: "npm:^8.0.0" + checksum: 10c0/5570a3a005b16f46c131968b8a5b56d291f9bbb85ff4217e31c80bd8a02e7de799e59a54b95ca28d5c302f248b54cbffde2d177c2f0f52ffcee7504c6eabf660 languageName: node linkType: hard -"jackspeak@npm:^4.1.1": - version: 4.1.1 - resolution: "jackspeak@npm:4.1.1" +"jest@npm:^29.7.0": + version: 29.7.0 + resolution: "jest@npm:29.7.0" dependencies: - "@isaacs/cliui": "npm:^8.0.2" - checksum: 10c0/84ec4f8e21d6514db24737d9caf65361511f75e5e424980eebca4199f400874f45e562ac20fa8aeb1dd20ca2f3f81f0788b6e9c3e64d216a5794fd6f30e0e042 + "@jest/core": "npm:^29.7.0" + "@jest/types": "npm:^29.6.3" + import-local: "npm:^3.0.2" + jest-cli: "npm:^29.7.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10c0/f40eb8171cf147c617cc6ada49d062fbb03b4da666cb8d39cdbfb739a7d75eea4c3ca150fb072d0d273dce0c753db4d0467d54906ad0293f59c54f9db4a09d8b languageName: node linkType: hard @@ -12861,6 +14256,13 @@ __metadata: languageName: node linkType: hard +"json-parse-even-better-errors@npm:^2.3.0": + version: 2.3.1 + resolution: "json-parse-even-better-errors@npm:2.3.1" + checksum: 10c0/140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 + languageName: node + linkType: hard + "json-schema-traverse@npm:^1.0.0": version: 1.0.0 resolution: "json-schema-traverse@npm:1.0.0" @@ -13001,6 +14403,13 @@ __metadata: languageName: node linkType: hard +"leven@npm:^3.1.0": + version: 3.1.0 + resolution: "leven@npm:3.1.0" + checksum: 10c0/cd778ba3fbab0f4d0500b7e87d1f6e1f041507c56fdcd47e8256a3012c98aaee371d4c15e0a76e0386107af2d42e2b7466160a2d80688aaa03e66e49949f42df + languageName: node + linkType: hard + "lilconfig@npm:^3.0.0, lilconfig@npm:^3.1.1, lilconfig@npm:^3.1.3": version: 3.1.3 resolution: "lilconfig@npm:3.1.3" @@ -13087,6 +14496,13 @@ __metadata: languageName: node linkType: hard +"lodash.memoize@npm:^4.1.2": + version: 4.1.2 + resolution: "lodash.memoize@npm:4.1.2" + checksum: 10c0/c8713e51eccc650422716a14cece1809cfe34bc5ab5e242b7f8b4e2241c2483697b971a604252807689b9dd69bfe3a98852e19a5b89d506b000b4187a1285df8 + languageName: node + linkType: hard + "lodash.once@npm:^4.0.0": version: 4.1.1 resolution: "lodash.once@npm:4.1.1" @@ -13234,7 +14650,16 @@ __metadata: languageName: node linkType: hard -"make-error@npm:^1.1.1": +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10c0/69b98a6c0b8e5c4fe9acb61608a9fbcfca1756d910f51e5dbe7a9e5cfb74fca9b8a0c8a0ffdf1294a740826c1ab4871d5bf3f62f72a3049e5eac6541ddffed68 + languageName: node + linkType: hard + +"make-error@npm:^1.1.1, make-error@npm:^1.3.6": version: 1.3.6 resolution: "make-error@npm:1.3.6" checksum: 10c0/171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f @@ -13260,6 +14685,15 @@ __metadata: languageName: node linkType: hard +"makeerror@npm:1.0.12": + version: 1.0.12 + resolution: "makeerror@npm:1.0.12" + dependencies: + tmpl: "npm:1.0.5" + checksum: 10c0/b0e6e599780ce6bab49cc413eba822f7d1f0dfebd1c103eaa3785c59e43e22c59018323cf9e1708f0ef5329e94a745d163fcbb6bff8e4c6742f9be9e86f3500c + languageName: node + linkType: hard + "map-cache@npm:^0.2.0": version: 0.2.2 resolution: "map-cache@npm:0.2.2" @@ -13343,7 +14777,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.8": +"micromatch@npm:^4.0.4, micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -13435,7 +14869,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.6": +"minimist@npm:^1.2.5, minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 @@ -13720,6 +15154,13 @@ __metadata: languageName: node linkType: hard +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 10c0/f5f9a7974bfb28a91afafa254b197f0f22c684d4a1731763dda960d2c8e375b36c7d690e0d9dc8fba774c537af14a7e979129bca23d88d052fbeb9466955e447 + languageName: node + linkType: hard + "natural-orderby@npm:^2.0.1": version: 2.0.3 resolution: "natural-orderby@npm:2.0.3" @@ -13748,6 +15189,13 @@ __metadata: languageName: node linkType: hard +"neo-async@npm:^2.6.2": + version: 2.6.2 + resolution: "neo-async@npm:2.6.2" + checksum: 10c0/c2f5a604a54a8ec5438a342e1f356dff4bc33ccccdb6dc668d94fe8e5eccfc9d2c2eea6064b0967a767ba63b33763f51ccf2cd2441b461a7322656c1f06b3f5d + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -14028,6 +15476,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:^3.1.0": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: "npm:^0.1.0" + checksum: 10c0/9db675949dbdc9c3763c89e748d0ef8bdad0afbb24d49ceaf4c46c02c77d30db4e0652ed36d0a0a7a95154335fab810d95c86153105bb73b3a90448e2bb14e1a + languageName: node + linkType: hard + "p-locate@npm:^4.1.0": version: 4.1.0 resolution: "p-locate@npm:4.1.0" @@ -14086,6 +15543,18 @@ __metadata: languageName: node linkType: hard +"parse-json@npm:^5.2.0": + version: 5.2.0 + resolution: "parse-json@npm:5.2.0" + dependencies: + "@babel/code-frame": "npm:^7.0.0" + error-ex: "npm:^1.3.1" + json-parse-even-better-errors: "npm:^2.3.0" + lines-and-columns: "npm:^1.1.6" + checksum: 10c0/77947f2253005be7a12d858aedbafa09c9ae39eb4863adf330f7b416ca4f4a08132e453e08de2db46459256fb66afaac5ee758b44fe6541b7cdaf9d252e59585 + languageName: node + linkType: hard + "parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -14341,7 +15810,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be @@ -14362,13 +15831,22 @@ __metadata: languageName: node linkType: hard -"pirates@npm:^4.0.1": +"pirates@npm:^4.0.1, pirates@npm:^4.0.4": version: 4.0.7 resolution: "pirates@npm:4.0.7" checksum: 10c0/a51f108dd811beb779d58a76864bbd49e239fa40c7984cd11596c75a121a8cc789f1c8971d8bb15f0dbf9d48b76c05bb62fcbce840f89b688c0fa64b37e8478a languageName: node linkType: hard +"pkg-dir@npm:^4.2.0": + version: 4.2.0 + resolution: "pkg-dir@npm:4.2.0" + dependencies: + find-up: "npm:^4.0.0" + checksum: 10c0/c56bda7769e04907a88423feb320babaed0711af8c436ce3e56763ab1021ba107c7b0cafb11cde7529f669cfc22bffcaebffb573645cbd63842ea9fb17cd7728 + languageName: node + linkType: hard + "pkg-types@npm:^1.3.0": version: 1.3.1 resolution: "pkg-types@npm:1.3.1" @@ -14567,6 +16045,17 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": + version: 29.7.0 + resolution: "pretty-format@npm:29.7.0" + dependencies: + "@jest/schemas": "npm:^29.6.3" + ansi-styles: "npm:^5.0.0" + react-is: "npm:^18.0.0" + checksum: 10c0/edc5ff89f51916f036c62ed433506b55446ff739358de77207e63e88a28ca2894caac6e73dcb68166a606e51c8087d32d400473e6a9fdd2dbe743f46c9c0276f + languageName: node + linkType: hard + "prism-react-renderer@npm:^2.0.6": version: 2.4.1 resolution: "prism-react-renderer@npm:2.4.1" @@ -14626,7 +16115,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.4.2": +"prompts@npm:^2.0.1, prompts@npm:^2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -14671,6 +16160,13 @@ __metadata: languageName: node linkType: hard +"pure-rand@npm:^6.0.0": + version: 6.1.0 + resolution: "pure-rand@npm:6.1.0" + checksum: 10c0/1abe217897bf74dcb3a0c9aba3555fe975023147b48db540aa2faf507aee91c03bf54f6aef0eb2bf59cc259a16d06b28eca37f0dc426d94f4692aeff02fb0e65 + languageName: node + linkType: hard + "qs@npm:6.13.0": version: 6.13.0 resolution: "qs@npm:6.13.0" @@ -14954,6 +16450,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^18.0.0": + version: 18.3.1 + resolution: "react-is@npm:18.3.1" + checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072 + languageName: node + linkType: hard + "react-json-view-lite@npm:^2.3.0": version: 2.4.2 resolution: "react-json-view-lite@npm:2.4.2" @@ -15317,6 +16820,13 @@ __metadata: languageName: node linkType: hard +"resolve.exports@npm:^2.0.0": + version: 2.0.3 + resolution: "resolve.exports@npm:2.0.3" + checksum: 10c0/1ade1493f4642a6267d0a5e68faeac20b3d220f18c28b140343feb83694d8fed7a286852aef43689d16042c61e2ddb270be6578ad4a13990769e12065191200d + languageName: node + linkType: hard + "resolve@npm:^1.1.7, resolve@npm:^1.20.0, resolve@npm:^1.22.8, resolve@npm:~1.22.1": version: 1.22.10 resolution: "resolve@npm:1.22.10" @@ -15530,7 +17040,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.0.0, semver@npm:^6.3.1": +"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -15539,7 +17049,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.5.4": +"semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.7.2": version: 7.7.2 resolution: "semver@npm:7.7.2" bin: @@ -15695,7 +17205,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3": +"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 @@ -15808,6 +17318,16 @@ __metadata: languageName: node linkType: hard +"source-map-support@npm:0.5.13": + version: 0.5.13 + resolution: "source-map-support@npm:0.5.13" + dependencies: + buffer-from: "npm:^1.0.0" + source-map: "npm:^0.6.0" + checksum: 10c0/137539f8c453fa0f496ea42049ab5da4569f96781f6ac8e5bfda26937be9494f4e8891f523c5f98f0e85f71b35d74127a00c46f83f6a4f54672b58d53202565e + languageName: node + linkType: hard + "source-map@npm:0.8.0-beta.0": version: 0.8.0-beta.0 resolution: "source-map@npm:0.8.0-beta.0" @@ -15817,6 +17337,13 @@ __metadata: languageName: node linkType: hard +"source-map@npm:^0.6.0, source-map@npm:^0.6.1": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + "split2@npm:^4.1.0": version: 4.2.0 resolution: "split2@npm:4.2.0" @@ -15863,6 +17390,15 @@ __metadata: languageName: node linkType: hard +"stack-utils@npm:^2.0.3": + version: 2.0.6 + resolution: "stack-utils@npm:2.0.6" + dependencies: + escape-string-regexp: "npm:^2.0.0" + checksum: 10c0/651c9f87667e077584bbe848acaecc6049bc71979f1e9a46c7b920cad4431c388df0f51b8ad7cfd6eed3db97a2878d0fc8b3122979439ea8bac29c61c95eec8a + languageName: node + linkType: hard + "standard-as-callback@npm:^2.1.0": version: 2.1.0 resolution: "standard-as-callback@npm:2.1.0" @@ -15891,6 +17427,16 @@ __metadata: languageName: node linkType: hard +"string-length@npm:^4.0.1": + version: 4.0.2 + resolution: "string-length@npm:4.0.2" + dependencies: + char-regex: "npm:^1.0.2" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/1cd77409c3d7db7bc59406f6bcc9ef0783671dcbabb23597a1177c166906ef2ee7c8290f78cae73a8aec858768f189d2cb417797df5e15ec4eb5e16b3346340c + languageName: node + linkType: hard + "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -15956,6 +17502,13 @@ __metadata: languageName: node linkType: hard +"strip-bom@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-bom@npm:4.0.0" + checksum: 10c0/26abad1172d6bc48985ab9a5f96c21e440f6e7e476686de49be813b5a59b3566dccb5c525b831ec54fe348283b47f3ffb8e080bc3f965fde12e84df23f6bb7ef + languageName: node + linkType: hard + "strip-final-newline@npm:^2.0.0": version: 2.0.0 resolution: "strip-final-newline@npm:2.0.0" @@ -15963,6 +17516,13 @@ __metadata: languageName: node linkType: hard +"strip-json-comments@npm:^3.1.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd + languageName: node + linkType: hard + "stripe@npm:^15.5.0": version: 15.12.0 resolution: "stripe@npm:15.12.0" @@ -16007,7 +17567,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.1.0, supports-color@npm:~8.1.1": +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.0, supports-color@npm:~8.1.1": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -16103,6 +17663,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^6.0.0": + version: 6.0.0 + resolution: "test-exclude@npm:6.0.0" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^7.1.4" + minimatch: "npm:^3.0.4" + checksum: 10c0/019d33d81adff3f9f1bfcff18125fb2d3c65564f437d9be539270ee74b994986abb8260c7c2ce90e8f30162178b09dbbce33c6389273afac4f36069c48521f57 + languageName: node + linkType: hard + "text-hex@npm:1.0.x": version: 1.0.0 resolution: "text-hex@npm:1.0.0" @@ -16168,6 +17739,13 @@ __metadata: languageName: node linkType: hard +"tmpl@npm:1.0.5": + version: 1.0.5 + resolution: "tmpl@npm:1.0.5" + checksum: 10c0/f935537799c2d1922cb5d6d3805f594388f75338fe7a4a9dac41504dd539704ca4db45b883b52e7b0aa5b2fd5ddadb1452bf95cd23a69da2f793a843f9451cc9 + languageName: node + linkType: hard + "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -16237,6 +17815,46 @@ __metadata: languageName: node linkType: hard +"ts-jest@npm:^29.2.5": + version: 29.4.1 + resolution: "ts-jest@npm:29.4.1" + dependencies: + bs-logger: "npm:^0.2.6" + fast-json-stable-stringify: "npm:^2.1.0" + handlebars: "npm:^4.7.8" + json5: "npm:^2.2.3" + lodash.memoize: "npm:^4.1.2" + make-error: "npm:^1.3.6" + semver: "npm:^7.7.2" + type-fest: "npm:^4.41.0" + yargs-parser: "npm:^21.1.1" + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/transform": ^29.0.0 || ^30.0.0 + "@jest/types": ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/transform": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + bin: + ts-jest: cli.js + checksum: 10c0/e4881717323c9e03ba9ad2f8726872cd0bede7f3f34095754aa850688b319f50294211cfd330edad878005e70601cbbbb0bb489ed0949a9aa545491e1083e923 + languageName: node + linkType: hard + "ts-node@npm:^10.9.2": version: 10.9.2 resolution: "ts-node@npm:10.9.2" @@ -16437,6 +18055,13 @@ __metadata: languageName: node linkType: hard +"type-detect@npm:4.0.8": + version: 4.0.8 + resolution: "type-detect@npm:4.0.8" + checksum: 10c0/8fb9a51d3f365a7de84ab7f73b653534b61b622aa6800aecdb0f1095a4a646d3f5eb295322127b6573db7982afcd40ab492d038cf825a42093a58b1e1353e0bd + languageName: node + linkType: hard + "type-fest@npm:^0.20.2": version: 0.20.2 resolution: "type-fest@npm:0.20.2" @@ -16451,7 +18076,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.0.0": +"type-fest@npm:^4.0.0, type-fest@npm:^4.41.0": version: 4.41.0 resolution: "type-fest@npm:4.41.0" checksum: 10c0/f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4 @@ -16520,6 +18145,15 @@ __metadata: languageName: node linkType: hard +"uglify-js@npm:^3.1.4": + version: 3.19.3 + resolution: "uglify-js@npm:3.19.3" + bin: + uglifyjs: bin/uglifyjs + checksum: 10c0/83b0a90eca35f778e07cad9622b80c448b6aad457c9ff8e568afed978212b42930a95f9e1be943a1ffa4258a3340fbb899f41461131c05bb1d0a9c303aed8479 + languageName: node + linkType: hard + "uid-safe@npm:~2.1.5": version: 2.1.5 resolution: "uid-safe@npm:2.1.5" @@ -16740,6 +18374,17 @@ __metadata: languageName: node linkType: hard +"v8-to-istanbul@npm:^9.0.1": + version: 9.3.0 + resolution: "v8-to-istanbul@npm:9.3.0" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.12" + "@types/istanbul-lib-coverage": "npm:^2.0.1" + convert-source-map: "npm:^2.0.0" + checksum: 10c0/968bcf1c7c88c04df1ffb463c179558a2ec17aa49e49376120504958239d9e9dad5281aa05f2a78542b8557f2be0b0b4c325710262f3b838b40d703d5ed30c23 + languageName: node + linkType: hard + "vary@npm:^1, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -16797,6 +18442,15 @@ __metadata: languageName: node linkType: hard +"walker@npm:^1.0.8": + version: 1.0.8 + resolution: "walker@npm:1.0.8" + dependencies: + makeerror: "npm:1.0.12" + checksum: 10c0/a17e037bccd3ca8a25a80cb850903facdfed0de4864bd8728f1782370715d679fa72e0a0f5da7c1c1379365159901e5935f35be531229da53bbfc0efdabdb48e + languageName: node + linkType: hard + "wcwidth@npm:^1.0.1": version: 1.0.1 resolution: "wcwidth@npm:1.0.1" @@ -16909,6 +18563,13 @@ __metadata: languageName: node linkType: hard +"wordwrap@npm:^1.0.0": + version: 1.0.0 + resolution: "wordwrap@npm:1.0.0" + checksum: 10c0/7ed2e44f3c33c5c3e3771134d2b0aee4314c9e49c749e37f464bf69f2bcdf0cbf9419ca638098e2717cff4875c47f56a007532f6111c3319f557a2ca91278e92 + languageName: node + linkType: hard + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -16961,6 +18622,16 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:^4.0.2": + version: 4.0.2 + resolution: "write-file-atomic@npm:4.0.2" + dependencies: + imurmurhash: "npm:^0.1.4" + signal-exit: "npm:^3.0.7" + checksum: 10c0/a2c282c95ef5d8e1c27b335ae897b5eca00e85590d92a3fd69a437919b7b93ff36a69ea04145da55829d2164e724bc62202cdb5f4b208b425aba0807889375c7 + languageName: node + linkType: hard + "xdg-basedir@npm:^4.0.0": version: 4.0.0 resolution: "xdg-basedir@npm:4.0.0" @@ -17078,7 +18749,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:17.7.2": +"yargs@npm:17.7.2, yargs@npm:^17.3.1": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: @@ -17134,6 +18805,13 @@ __metadata: languageName: node linkType: hard +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f + languageName: node + linkType: hard + "yoctocolors-cjs@npm:^2.1.2": version: 2.1.2 resolution: "yoctocolors-cjs@npm:2.1.2" From 25c6c589eaaaa1a119cb69d0416551992f098884 Mon Sep 17 00:00:00 2001 From: Derek Wene Date: Thu, 14 Aug 2025 11:55:03 -0500 Subject: [PATCH 3/5] chore: more tests --- .../src/core/__tests__/braintree-base.spec.ts | 98 ++++++++++++++++++- 1 file changed, 94 insertions(+), 4 deletions(-) diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts index 38c0db64..388bc92d 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts @@ -1,9 +1,5 @@ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; -// Minimal fallbacks to avoid depending on framework type packages in tests -const ContainerRegistrationKeys = { LOGGER: 'logger' } as const; -const Modules = { CACHE: 'cache' } as const; import BraintreeProviderService from '../../services/braintree-provider'; -import { MedusaContainer } from '@medusajs/framework/types'; import { BraintreeConstructorArgs, BraintreePaymentSessionData } from '../braintree-base'; const buildService = () => { @@ -161,6 +157,100 @@ describe('BraintreeProviderService core behaviors', () => { expect((result.data as any)?.braintreeRefund?.success).toBe(true); }); + it('refundPayment voids when transaction is submitted_for_settlement', async () => { + const { service, gateway } = buildService(); + + const input = { + amount: 10, // standard unit + data: { + clientToken: 'ct', + amount: 1000, + currency_code: 'USD', + braintreeTransaction: { id: 't1' }, + }, + } as any; + + gateway.transaction.find.mockResolvedValueOnce({ id: 't1', status: 'submitted_for_settlement' }); + gateway.transaction.void.mockResolvedValueOnce({ success: true }); + gateway.transaction.find.mockResolvedValueOnce({ id: 't1', status: 'voided' }); + + const result = await service.refundPayment(input); + + expect(gateway.transaction.void).toHaveBeenCalledWith('t1'); + expect((result.data as any)?.braintreeRefund?.success).toBe(true); + }); + + it('refundPayment refunds when transaction is settling', async () => { + const { service, gateway } = buildService(); + + const input = { + amount: 7.5, // standard unit => 750 smallest => "7.50" decimal + data: { + clientToken: 'ct', + amount: 1000, + currency_code: 'USD', + braintreeTransaction: { id: 't2' }, + }, + } as any; + + gateway.transaction.find + .mockResolvedValueOnce({ id: 't2', status: 'settling' }) + .mockResolvedValueOnce({ id: 't2', status: 'settling' }); + gateway.transaction.refund.mockResolvedValueOnce({ transaction: { id: 'r2' } }); + + const result = await service.refundPayment(input); + + expect(gateway.transaction.refund).toHaveBeenCalledWith('t2', '7.50'); + expect((result.data as any)?.braintreeRefund?.id).toBe('r2'); + }); + + it('refundPayment throws for non-refundable statuses', async () => { + const { service, gateway } = buildService(); + + const input = { + amount: 5, + data: { + clientToken: 'ct', + amount: 1000, + currency_code: 'USD', + braintreeTransaction: { id: 't3' }, + }, + } as any; + + gateway.transaction.find.mockResolvedValueOnce({ id: 't3', status: 'failed' }); + + await expect(service.refundPayment(input)).rejects.toThrow(); + expect(gateway.transaction.void).not.toHaveBeenCalled(); + expect(gateway.transaction.refund).not.toHaveBeenCalled(); + }); + + it('refundPayment importRefundedAmount hack skips provider refund and updates refundedTotal', async () => { + const { service, gateway } = buildService(); + + const input = { + amount: 5, // standard unit => refundAmount = 500 (USD) + data: { + clientToken: 'ct', + amount: 1000, + currency_code: 'USD', + braintreeTransaction: { id: 't4' }, + importRefundedAmount: 500, + refundedTotal: 200, + }, + } as any; + + gateway.transaction.find.mockResolvedValueOnce({ id: 't4', status: 'settled' }); + + const result = await service.refundPayment(input); + + expect(gateway.transaction.void).not.toHaveBeenCalled(); + expect(gateway.transaction.refund).not.toHaveBeenCalled(); + + const data = result.data as any; + expect(data.refundedTotal).toBe(200 + 500); + expect(data.braintreeTransaction?.id).toBe('t4'); + }); + it('refundPayment refunds with decimal string when transaction is settled', async () => { const { service, gateway } = buildService(); From 4b57dadb23960540591e6d32560f5fd3a5eba796 Mon Sep 17 00:00:00 2001 From: Derek Wene Date: Fri, 15 Aug 2025 07:35:14 -0500 Subject: [PATCH 4/5] feat: add import braintree provider --- plugins/braintree-payment/package.json | 2 +- .../src/core/__tests__/braintree-base.spec.ts | 39 +-- .../core/__tests__/braintree-import.spec.ts | 92 +++++++ .../src/core/braintree-base.ts | 42 ++-- .../src/core/braintree-import.ts | 226 ++++++++++++++++++ .../providers/payment-braintree/src/index.ts | 4 +- .../src/services/braintree-import.ts | 16 ++ .../payment-braintree/src/services/index.ts | 1 + .../payment-braintree/src/types/index.ts | 1 + .../utils/__tests__/get-smallest-unit.spec.ts | 22 -- .../src/utils/get-smallest-unit.ts | 91 ------- 11 files changed, 360 insertions(+), 176 deletions(-) create mode 100644 plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-import.spec.ts create mode 100644 plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-import.ts create mode 100644 plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-import.ts delete mode 100644 plugins/braintree-payment/src/providers/payment-braintree/src/utils/__tests__/get-smallest-unit.spec.ts delete mode 100644 plugins/braintree-payment/src/providers/payment-braintree/src/utils/get-smallest-unit.ts diff --git a/plugins/braintree-payment/package.json b/plugins/braintree-payment/package.json index 1f859303..1a5071bd 100644 --- a/plugins/braintree-payment/package.json +++ b/plugins/braintree-payment/package.json @@ -1,6 +1,6 @@ { "name": "@lambdacurry/medusa-payment-braintree", - "version": "0.0.8", + "version": "0.0.9-alpha.5", "description": "Braintree plugin for Medusa", "author": "Lambda Curry (https://lambdacurry.dev)", "license": "MIT", diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts index 388bc92d..94178bb4 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts @@ -82,13 +82,13 @@ describe('BraintreeProviderService core behaviors', () => { expect(setArgs[2]).toBe(24 * 3600 - 1); }); - it('authorizePayment creates a sale with decimal string amount and returns captured when autoCapture=true', async () => { + it('authorizePayment creates a sale with decimal string amount (2dp) and returns captured when autoCapture=true', async () => { const { service, gateway } = buildService(); const input = { data: { clientToken: 'ct', - amount: 1000, // smallest unit + amount: 10, // standard unit -> "10.00" currency_code: 'USD', paymentMethodNonce: 'fake-nonce', }, @@ -180,11 +180,11 @@ describe('BraintreeProviderService core behaviors', () => { expect((result.data as any)?.braintreeRefund?.success).toBe(true); }); - it('refundPayment refunds when transaction is settling', async () => { + it('refundPayment refunds with 2dp when transaction is settling', async () => { const { service, gateway } = buildService(); const input = { - amount: 7.5, // standard unit => 750 smallest => "7.50" decimal + amount: 7.5, // -> "7.50" data: { clientToken: 'ct', amount: 1000, @@ -224,38 +224,13 @@ describe('BraintreeProviderService core behaviors', () => { expect(gateway.transaction.refund).not.toHaveBeenCalled(); }); - it('refundPayment importRefundedAmount hack skips provider refund and updates refundedTotal', async () => { - const { service, gateway } = buildService(); - - const input = { - amount: 5, // standard unit => refundAmount = 500 (USD) - data: { - clientToken: 'ct', - amount: 1000, - currency_code: 'USD', - braintreeTransaction: { id: 't4' }, - importRefundedAmount: 500, - refundedTotal: 200, - }, - } as any; - - gateway.transaction.find.mockResolvedValueOnce({ id: 't4', status: 'settled' }); - - const result = await service.refundPayment(input); - - expect(gateway.transaction.void).not.toHaveBeenCalled(); - expect(gateway.transaction.refund).not.toHaveBeenCalled(); - - const data = result.data as any; - expect(data.refundedTotal).toBe(200 + 500); - expect(data.braintreeTransaction?.id).toBe('t4'); - }); + // NOTE: Import/refund simulation moved to the dedicated import provider tests - it('refundPayment refunds with decimal string when transaction is settled', async () => { + it('refundPayment refunds with 2dp decimal string when transaction is settled', async () => { const { service, gateway } = buildService(); const input = { - amount: 5, // standard unit => 500 smallest => "5.00" decimal + amount: 5.001, // -> "5.00" data: { clientToken: 'ct', amount: 1000, diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-import.spec.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-import.spec.ts new file mode 100644 index 00000000..1e4848cc --- /dev/null +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-import.spec.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, jest, beforeEach } from '@jest/globals'; +import BraintreeImportService from '../../services/braintree-import'; +import { BraintreeConstructorArgs } from '../braintree-base'; + +const buildService = () => { + const logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() } as any; + const cache = { get: jest.fn(), set: jest.fn() } as any; + + const container: BraintreeConstructorArgs = { logger, cache }; + + const options = { + environment: 'sandbox' as const, + merchantId: 'merchant', + publicKey: 'public', + privateKey: 'private', + enable3DSecure: false, + savePaymentMethod: false, + webhookSecret: 'whsec', + autoCapture: true, + } as any; + + const service = new BraintreeImportService(container, options); + + // Replace gateway used only for actual refunds + const gateway = { + transaction: { + find: jest.fn(), + void: jest.fn(), + refund: jest.fn(), + }, + } as any; + + (service as any).gateway = gateway; + + return { service, gateway }; +}; + +describe('BraintreeImportService', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('simulates authorize/capture without API calls', async () => { + const { service, gateway } = buildService(); + const init = await service.initiatePayment({ + data: { transactionId: 't1' }, + amount: 10, + currency_code: 'USD', + context: { idempotency_key: 'idem' }, + } as any); + expect(init.id).toBeDefined(); + + const auth = await service.authorizePayment({ data: init.data } as any); + expect(auth.status).toBe('authorized'); + expect(gateway.transaction.find).not.toHaveBeenCalled(); + + const cap = await service.capturePayment({ data: auth.data } as any); + expect((cap.data as any).status).toBe('captured'); + }); + + it('simulates refund when importedAsRefunded=true', async () => { + const { service, gateway } = buildService(); + const session = { transactionId: 't1', importedAsRefunded: true, refundedTotal: 2, status: 'captured' } as any; + + const res = await service.refundPayment({ amount: 3, data: session } as any); + expect((res.data as any).refundedTotal).toBe(5); + expect(gateway.transaction.void).not.toHaveBeenCalled(); + expect(gateway.transaction.refund).not.toHaveBeenCalled(); + }); + + it('performs real void for authorized/submitted_for_settlement when not imported-refunded', async () => { + const { service, gateway } = buildService(); + const session = { transactionId: 't2', importedAsRefunded: false, refundedTotal: 0, status: 'captured' } as any; + gateway.transaction.find.mockResolvedValueOnce({ id: 't2', status: 'authorized' }); + gateway.transaction.void.mockResolvedValueOnce({ success: true }); + + const res = await service.refundPayment({ amount: 10, data: session } as any); + expect(gateway.transaction.void).toHaveBeenCalledWith('t2'); + expect((res.data as any).refundedTotal).toBe(10); + }); + + it('performs real refund for settled/settling when not imported-refunded', async () => { + const { service, gateway } = buildService(); + const session = { transactionId: 't3', importedAsRefunded: false, refundedTotal: 1.25, status: 'captured' } as any; + gateway.transaction.find.mockResolvedValueOnce({ id: 't3', status: 'settled' }); + gateway.transaction.refund.mockResolvedValueOnce({ transaction: { id: 'r3' } }); + + const res = await service.refundPayment({ amount: 2.75, data: session } as any); + expect(gateway.transaction.refund).toHaveBeenCalledWith('t3', '2.75'); + expect((res.data as any).refundedTotal).toBe(4.0); + }); +}); diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts index 108f274e..00aa19da 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts @@ -46,7 +46,6 @@ import type { import type { Transaction, TransactionNotification, TransactionStatus } from 'braintree'; import Braintree from 'braintree'; import type { BraintreeOptions, CustomFields } from '../types'; -import { getSmallestUnit, formatSmallestUnitToDecimalString } from '../utils/get-smallest-unit'; export type BraintreeConstructorArgs = Record & { logger: Logger; @@ -65,7 +64,6 @@ export interface BraintreePaymentSessionData { export interface BraintreeInitiatePaymentData { transactionId?: string; - previouslyRefundedAmount?: number; paymentMethodNonce?: string; } @@ -88,6 +86,7 @@ class BraintreeBase extends AbstractPaymentProvider { } async saveClientTokenToCache(clientToken: string, customerId: string, expiresOnEpochSeconds: number): Promise { + if (!customerId) throw new MedusaError(MedusaError.Types.INVALID_ARGUMENT, 'Customer ID is required'); const nowSeconds = Math.floor(Date.now() / 1000); const ttlSeconds = expiresOnEpochSeconds - nowSeconds - 1; if (!customerId || !clientToken || ttlSeconds <= 0) return; @@ -124,7 +123,6 @@ class BraintreeBase extends AbstractPaymentProvider { paymentMethodNonce: z.string().optional(), braintreeTransaction: z.any().optional(), refundedTotal: z.number().optional(), - importRefundedAmount: z.number().optional(), }); const result = schema.safeParse(data); @@ -155,8 +153,12 @@ class BraintreeBase extends AbstractPaymentProvider { }); } + private formatToTwoDecimalString(amount: number): string { + const rounded = Math.round(Number(amount) * 100) / 100; + return rounded.toFixed(2); + } + static validateOptions(options: BraintreeOptions): void { - // Required string fields const requiredFields = ['merchantId', 'publicKey', 'privateKey', 'webhookSecret', 'environment']; for (const field of requiredFields) { @@ -176,12 +178,10 @@ class BraintreeBase extends AbstractPaymentProvider { ); } - // Optional boolean fields with defaults options.enable3DSecure = options.enable3DSecure ?? false; options.savePaymentMethod = options.savePaymentMethod ?? false; options.autoCapture = options.autoCapture ?? false; - // Type check boolean fields const booleanFields = ['enable3DSecure', 'savePaymentMethod', 'autoCapture']; for (const field of booleanFields) { if (isDefined(options[field]) && typeof options[field] !== 'boolean') { @@ -429,9 +429,9 @@ class BraintreeBase extends AbstractPaymentProvider { clientToken: token, medusaPaymentSessionId: paymentSessionId as string, paymentMethodNonce: data?.paymentMethodNonce as string, - amount: getSmallestUnit(input.amount, input.currency_code), + amount: Number(input.amount), currency_code: input.currency_code, - importRefundedAmount: data.previouslyRefundedAmount, + refundedTotal: 0, }; @@ -448,7 +448,7 @@ class BraintreeBase extends AbstractPaymentProvider { }): Promise { const sessionData = await this.parsePaymentSessionData(input.data ?? {}); - const toPayDecimal = formatSmallestUnitToDecimalString(sessionData.amount, sessionData.currency_code); + const toPayDecimal = this.formatToTwoDecimalString(Number(sessionData.amount)); const braintreeTransactionCreateRequest = this.getBraintreeTransactionCreateRequestBody({ amount: toPayDecimal, @@ -567,24 +567,10 @@ class BraintreeBase extends AbstractPaymentProvider { async refundPayment(input: RefundPaymentInput): Promise { const sessionData = await this.parsePaymentSessionData(input.data ?? {}); - const refundAmount = getSmallestUnit(input.amount, sessionData.currency_code); + const refundAmount = Number(input.amount); + const refundAmountRounded = Number(this.formatToTwoDecimalString(refundAmount)); - const previouslyRefundedAmount = sessionData.refundedTotal ?? 0; - - if (sessionData.importRefundedAmount && refundAmount === sessionData.importRefundedAmount) { - // This is a hack so that we can import braintree transactions that have already been refunded. - // We basically skip the refund process here and just pretend that we refunded the amount. - const updatedTransaction = await this.retrieveTransaction(sessionData.braintreeTransaction!.id); - - const refundResult: RefundPaymentOutput = { - data: { - ...input.data, - braintreeTransaction: updatedTransaction, - refundedTotal: previouslyRefundedAmount + refundAmount, - }, - }; - return refundResult; - } + const previouslyRefundedAmount = Number(this.formatToTwoDecimalString(sessionData.refundedTotal ?? 0)); const braintreeTransaction = await this.retrieveTransaction(sessionData.braintreeTransaction?.id as string); @@ -614,7 +600,7 @@ class BraintreeBase extends AbstractPaymentProvider { } if (braintreeTransaction.id) { - const refundAmountDecimal = formatSmallestUnitToDecimalString(refundAmount, sessionData.currency_code); + const refundAmountDecimal = this.formatToTwoDecimalString(refundAmountRounded); try { const { transaction: refundTransaction } = await this.gateway.transaction.refund( braintreeTransaction.id, @@ -628,7 +614,7 @@ class BraintreeBase extends AbstractPaymentProvider { ...input.data, braintreeTransaction: updatedTransaction, braintreeRefund: refundTransaction, - refundedTotal: previouslyRefundedAmount + refundAmount, + refundedTotal: Number(this.formatToTwoDecimalString(previouslyRefundedAmount + refundAmountRounded)), }, }; return refundResult; diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-import.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-import.ts new file mode 100644 index 00000000..e877281a --- /dev/null +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-import.ts @@ -0,0 +1,226 @@ +import { AbstractPaymentProvider, MedusaError, PaymentActions, PaymentSessionStatus } from '@medusajs/framework/utils'; +import { z } from 'zod'; +import { BraintreeOptions, PaymentProviderKeys } from '../types'; +import type { BraintreeConstructorArgs } from './braintree-base'; +import Braintree from 'braintree'; +import { + CapturePaymentInput, + CapturePaymentOutput, + AuthorizePaymentInput, + AuthorizePaymentOutput, + CancelPaymentInput, + CancelPaymentOutput, + InitiatePaymentInput, + InitiatePaymentOutput, + DeletePaymentInput, + DeletePaymentOutput, + GetPaymentStatusInput, + GetPaymentStatusOutput, + RefundPaymentInput, + RefundPaymentOutput, + RetrievePaymentInput, + RetrievePaymentOutput, + UpdatePaymentInput, + UpdatePaymentOutput, + ProviderWebhookPayload, + WebhookActionResult, + DeleteAccountHolderOutput, + CreateAccountHolderInput, + CreateAccountHolderOutput, + DeleteAccountHolderInput, +} from '@medusajs/types'; + +export interface BraintreeImportInitiatePaymentData { + transactionId?: string; + importedAsRefunded?: boolean; +} + +export interface BraintreeImportPaymentSessionData { + transactionId?: string; + refundedTotal?: number; + importedAsRefunded?: boolean; + status: PaymentSessionStatus; +} + +class BraintreeImport extends AbstractPaymentProvider { + static identifier = PaymentProviderKeys.IMPORTED; + options: BraintreeOptions; + private gateway: Braintree.BraintreeGateway; + + constructor(container: BraintreeConstructorArgs, options: BraintreeOptions) { + super(container, options); + this.options = options; + // Create a lightweight Braintree gateway for refund operations only + const envKey = (this.options.environment || 'sandbox').toLowerCase(); + const envMap: Record = { + qa: Braintree.Environment.Qa, + sandbox: Braintree.Environment.Sandbox, + production: Braintree.Environment.Production, + development: Braintree.Environment.Development, + }; + const environment = envMap[envKey] ?? Braintree.Environment.Sandbox; + + this.gateway = new Braintree.BraintreeGateway({ + environment, + merchantId: this.options.merchantId!, + publicKey: this.options.publicKey!, + privateKey: this.options.privateKey!, + }); + } + + private parseInitiateData(data: Record): BraintreeImportInitiatePaymentData { + const schema = z.object({ + transactionId: z.string().optional(), + importedAsRefunded: z.boolean().optional().default(false), + }); + const result = schema.safeParse(data ?? {}); + if (!result.success) { + throw new MedusaError(MedusaError.Types.INVALID_ARGUMENT, result.error.message); + } + return result.data as BraintreeImportInitiatePaymentData; + } + + private parseSessionData(data: Record): BraintreeImportPaymentSessionData { + const schema = z.object({ + transactionId: z.string().optional(), + refundedTotal: z.number().optional().default(0), + importedAsRefunded: z.boolean().optional().default(false), + status: z.nativeEnum(PaymentSessionStatus).optional().default(PaymentSessionStatus.PENDING), + }); + const result = schema.safeParse(data ?? {}); + if (!result.success) { + throw new MedusaError(MedusaError.Types.INVALID_ARGUMENT, result.error.message); + } + return result.data as BraintreeImportPaymentSessionData; + } + + private formatToTwoDecimalString(amount: number): string { + const rounded = Math.round(Number(amount) * 100) / 100; + return rounded.toFixed(2); + } + + async initiatePayment(input: InitiatePaymentInput): Promise { + const data = this.parseInitiateData(input.data ?? {}); + const id = input.context?.idempotency_key ?? crypto.randomUUID(); + + const session: BraintreeImportPaymentSessionData = { + transactionId: data.transactionId, + refundedTotal: 0, + importedAsRefunded: data.importedAsRefunded ?? false, + status: PaymentSessionStatus.PENDING, + }; + + return { id, data: { ...session } }; + } + + async getPaymentStatus(input: GetPaymentStatusInput): Promise { + const session = this.parseSessionData(input.data ?? {}); + return { status: session.status }; + } + + async retrievePayment(input: RetrievePaymentInput): Promise { + return { data: { ...input.data } }; + } + + async authorizePayment(input: AuthorizePaymentInput): Promise { + const session = this.parseSessionData(input.data ?? {}); + const updated: BraintreeImportPaymentSessionData = { ...session, status: PaymentSessionStatus.AUTHORIZED }; + return { data: { ...updated }, status: PaymentSessionStatus.AUTHORIZED }; + } + + async updatePayment(input: UpdatePaymentInput): Promise { + return { data: { ...input.data } }; + } + + async deletePayment(input: DeletePaymentInput): Promise { + const session = this.parseSessionData(input.data ?? {}); + return { data: { ...session, status: PaymentSessionStatus.CANCELED } }; + } + + async capturePayment(input: CapturePaymentInput): Promise { + const session = this.parseSessionData(input.data ?? {}); + return { data: { ...session, status: PaymentSessionStatus.CAPTURED } }; + } + + async createAccountHolder(input: CreateAccountHolderInput): Promise { + return { id: input.context.customer.id }; + } + + async deleteAccountHolder(input: DeleteAccountHolderInput): Promise { + return { data: {} }; + } + + async refundPayment(input: RefundPaymentInput): Promise { + const session = this.parseSessionData(input.data ?? {}); + + const refundAmount = Number(input.amount); + const refundAmountRounded = Number(this.formatToTwoDecimalString(refundAmount)); + const previouslyRefunded = Number(this.formatToTwoDecimalString(session.refundedTotal ?? 0)); + + // If the order was imported as already refunded, simulate refund without hitting Braintree + if (session.importedAsRefunded) { + return { + data: { + ...session, + refundedTotal: Number(this.formatToTwoDecimalString(previouslyRefunded + refundAmountRounded)), + }, + }; + } + + // Otherwise, perform a real refund/void against Braintree using the provided transaction id + const transactionId = session.transactionId; + if (!transactionId) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + 'transactionId is required on session data to perform a refund', + ); + } + + const braintreeTransaction = await this.gateway.transaction.find(transactionId); + + const shouldVoid = ['submitted_for_settlement', 'authorized'].includes(braintreeTransaction.status); + if (shouldVoid) { + const cancelledTransaction = await this.gateway.transaction.void(braintreeTransaction.id); + return { + data: { + ...session, + braintreeRefund: cancelledTransaction, + refundedTotal: Number(this.formatToTwoDecimalString(previouslyRefunded + refundAmountRounded)), + }, + }; + } + + const shouldRefund = ['settled', 'settling'].includes(braintreeTransaction.status); + if (!shouldRefund) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Braintree transaction with ID ${braintreeTransaction.id} cannot be refunded`, + ); + } + + const refundAmountDecimal = this.formatToTwoDecimalString(refundAmountRounded); + const { transaction: refundTransaction } = await this.gateway.transaction.refund( + braintreeTransaction.id, + refundAmountDecimal, + ); + + return { + data: { + ...session, + braintreeRefund: refundTransaction, + refundedTotal: Number(this.formatToTwoDecimalString(previouslyRefunded + refundAmountRounded)), + }, + }; + } + + async cancelPayment(input: CancelPaymentInput): Promise { + const session = this.parseSessionData(input.data ?? {}); + return { data: { ...session, status: PaymentSessionStatus.CANCELED } }; + } + + async getWebhookActionAndData(_payload: ProviderWebhookPayload['payload']): Promise { + return { action: PaymentActions.NOT_SUPPORTED }; + } +} + +export default BraintreeImport; diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/index.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/index.ts index c72ac1d8..a6364277 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/index.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/index.ts @@ -1,7 +1,7 @@ import { ModuleProvider, Modules } from '@medusajs/framework/utils'; -import { BraintreeProviderService } from './services'; +import { BraintreeProviderService, BraintreeImportService } from './services'; -const services = [BraintreeProviderService]; +const services = [BraintreeProviderService, BraintreeImportService]; export default ModuleProvider(Modules.PAYMENT, { services, diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-import.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-import.ts new file mode 100644 index 00000000..3e3de9e8 --- /dev/null +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-import.ts @@ -0,0 +1,16 @@ +import type { BraintreeOptions } from '../types'; +import { PaymentProviderKeys } from '../types'; +import { BraintreeConstructorArgs } from '../core/braintree-base'; +import BraintreeImport from '../core/braintree-import'; + +class BraintreeImportService extends BraintreeImport { + static identifier = PaymentProviderKeys.IMPORTED; + options: BraintreeOptions; + + constructor(container: BraintreeConstructorArgs, options: BraintreeOptions) { + super(container, options); + this.options = options; + } +} + +export default BraintreeImportService; diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/services/index.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/services/index.ts index 6f46805d..28358ea2 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/services/index.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/services/index.ts @@ -1 +1,2 @@ export { default as BraintreeProviderService } from './braintree-provider'; +export { default as BraintreeImportService } from './braintree-import'; diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts index bbe43802..5a8206e7 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts @@ -13,6 +13,7 @@ export interface BraintreeOptions extends Braintree.ClientGatewayConfig { export const PaymentProviderKeys = { BRAINTREE: 'braintree', + IMPORTED: 'imported', }; export interface CustomFields { diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/utils/__tests__/get-smallest-unit.spec.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/utils/__tests__/get-smallest-unit.spec.ts deleted file mode 100644 index 0a642094..00000000 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/utils/__tests__/get-smallest-unit.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { getSmallestUnit } from '../get-smallest-unit'; - -describe('getSmallestUnit', () => { - it('should convert an amount to the format required by Braintree based on currency', () => { - // 0 decimals - expect(getSmallestUnit(50098, 'JPY')).toBe(50098); - - // 3 decimals - expect(getSmallestUnit(5.124, 'KWD')).toBe(5130); - - // 2 decimals - expect(getSmallestUnit(2.675, 'USD')).toBe(268); - - expect(getSmallestUnit(100.54, 'USD')).toBe(10054); - expect(getSmallestUnit(5.126, 'KWD')).toBe(5130); - expect(getSmallestUnit(0.54, 'USD')).toBe(54); - expect(getSmallestUnit(0.054, 'USD')).toBe(5); - expect(getSmallestUnit(0.005104, 'USD')).toBe(1); - expect(getSmallestUnit(0.004104, 'USD')).toBe(0); - }); -}); diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/utils/get-smallest-unit.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/utils/get-smallest-unit.ts deleted file mode 100644 index 461f48dc..00000000 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/utils/get-smallest-unit.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { BigNumberInput } from '@medusajs/framework/types'; -import { BigNumber, MathBN } from '@medusajs/framework/utils'; - -function getCurrencyMultiplier(currency: string): number { - 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'], - }; - - const uppercaseCurrency = currency.toUpperCase(); - let power = 2; - for (const [key, value] of Object.entries(currencyMultipliers)) { - if (value.includes(uppercaseCurrency)) { - power = Number.parseInt(key, 10); - break; - } - } - return 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 amount_ = Math.round(new BigNumber(MathBN.mult(amount, multiplier)).numeric) / multiplier; - - 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 parseInt(numeric.toString().split(".").shift()!, 10); - const splitResult = numeric.toString().split('.'); - const integerPart = splitResult.shift(); - - if (!integerPart) { - throw new Error(`Failed to parse numeric value: ${numeric}`); - } - - return Number.parseInt(integerPart, 10); -} - -/** - * 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; -} - -/** - * Formats an amount provided in the smallest currency unit into a decimal string - * suitable for providers (e.g., Braintree) that expect standard unit decimal strings. - * - * Examples: - * - USD: 1234 -> "12.34" - * - JPY: 1234 -> "1234" - * - JOD (3 decimals): 12340 -> "12.340" - */ -export function formatSmallestUnitToDecimalString(amount: BigNumberInput, currency: string): string { - const multiplier = getCurrencyMultiplier(currency); - - // Determine number of fraction digits based on multiplier (10^digits) - let fractionDigits = 0; - if (multiplier === 1000) { - fractionDigits = 3; - } else if (multiplier === 100) { - fractionDigits = 2; - } else if (multiplier === 1) { - fractionDigits = 0; - } else { - // Fallback: infer by counting zeros in multiplier - fractionDigits = Math.max(0, Math.round(Math.log10(multiplier))); - } - - const standardAmount = getAmountFromSmallestUnit(amount, currency); - return standardAmount.toFixed(fractionDigits); -} From 9ed3fe5685bf96df8a8d2215209e2641078e10cc Mon Sep 17 00:00:00 2001 From: Derek Wene Date: Fri, 15 Aug 2025 07:36:51 -0500 Subject: [PATCH 5/5] chore: bump version --- plugins/braintree-payment/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/braintree-payment/package.json b/plugins/braintree-payment/package.json index 1a5071bd..87afe73f 100644 --- a/plugins/braintree-payment/package.json +++ b/plugins/braintree-payment/package.json @@ -1,6 +1,6 @@ { "name": "@lambdacurry/medusa-payment-braintree", - "version": "0.0.9-alpha.5", + "version": "0.0.9", "description": "Braintree plugin for Medusa", "author": "Lambda Curry (https://lambdacurry.dev)", "license": "MIT",