From 02fe59cba7d9d1118ccd23a0ef8a66be1739cb16 Mon Sep 17 00:00:00 2001 From: Amitabh Aggarwal Date: Tue, 16 Jun 2026 12:17:56 -0500 Subject: [PATCH] fix(ramps-controller): compare internal order ids during order processing --- packages/ramps-controller/CHANGELOG.md | 4 ++ .../RampsController-method-action-types.ts | 2 +- .../src/RampsController.test.ts | 71 ++++++++++++++++++- .../ramps-controller/src/RampsController.ts | 52 +++++++++++--- packages/ramps-controller/src/index.ts | 1 + 5 files changed, 119 insertions(+), 11 deletions(-) diff --git a/packages/ramps-controller/CHANGELOG.md b/packages/ramps-controller/CHANGELOG.md index 3360cedbca..c4cb800e0b 100644 --- a/packages/ramps-controller/CHANGELOG.md +++ b/packages/ramps-controller/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/profile-sync-controller` from `^28.1.1` to `^28.2.0` ([#9119](https://github.com/MetaMask/core/pull/9119)) +### Fixed + +- Compare internal order codes (from canonical order `id`) instead of provider-native `providerOrderId` when merging orders in `RampsController.addOrder` and `RampsController.getOrder` ([#TBD](https://github.com/MetaMask/core/pull/TBD)) + ## [14.2.0] ### Changed diff --git a/packages/ramps-controller/src/RampsController-method-action-types.ts b/packages/ramps-controller/src/RampsController-method-action-types.ts index 41d5a884af..c04fba0716 100644 --- a/packages/ramps-controller/src/RampsController-method-action-types.ts +++ b/packages/ramps-controller/src/RampsController-method-action-types.ts @@ -229,7 +229,7 @@ export type RampsControllerGetQuotesAction = { /** * Adds or updates a V2 order in controller state. - * If an order with the same providerOrderId already exists, the incoming + * If an order with the same internal order code already exists, the incoming * fields are merged on top of the existing order so that fields not present * in the update (e.g. paymentDetails from the Transak API) are preserved. * diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index 7e1fd0d1b9..72578a77c0 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -7100,12 +7100,39 @@ describe('RampsController', () => { rootMessenger.call('RampsController:addOrder', mockOrder); rootMessenger.call('RampsController:addOrder', { ...mockOrder, + id: '/providers/transak-staging/orders/def-456', providerOrderId: 'def-456', }); expect(controller.state.orders).toHaveLength(2); }); }); + + it('merges orders using internal order id when providerOrderId differs', async () => { + await withController(({ controller, rootMessenger }) => { + const precreatedOrder = createMockOrder({ + id: '/providers/paypal/orders/internal-order-123', + providerOrderId: 'internal-order-123', + status: RampsOrderStatus.Precreated, + }); + rootMessenger.call('RampsController:addOrder', precreatedOrder); + + const apiOrder = createMockOrder({ + id: '/providers/paypal/orders/internal-order-123', + providerOrderId: 'provider-native-order-456', + status: RampsOrderStatus.Pending, + }); + rootMessenger.call('RampsController:addOrder', apiOrder); + + expect(controller.state.orders).toHaveLength(1); + expect(controller.state.orders[0]?.status).toBe( + RampsOrderStatus.Pending, + ); + expect(controller.state.orders[0]?.providerOrderId).toBe( + 'internal-order-123', + ); + }); + }); }); describe('removeOrder', () => { @@ -7334,6 +7361,42 @@ describe('RampsController', () => { }); }); + it('merges precreated order when API returns a different providerOrderId', async () => { + await withController(async ({ controller, rootMessenger }) => { + const precreatedOrder = createMockOrder({ + id: '/providers/paypal/orders/internal-order-123', + providerOrderId: 'internal-order-123', + status: RampsOrderStatus.Precreated, + }); + rootMessenger.call('RampsController:addOrder', precreatedOrder); + + const apiOrder = createMockOrder({ + id: '/providers/paypal/orders/internal-order-123', + providerOrderId: 'provider-native-order-456', + status: RampsOrderStatus.Pending, + }); + rootMessenger.registerActionHandler( + 'RampsService:getOrder', + async () => apiOrder, + ); + + await rootMessenger.call( + 'RampsController:getOrder', + 'paypal', + 'internal-order-123', + '0xabc', + ); + + expect(controller.state.orders).toHaveLength(1); + expect(controller.state.orders[0]?.status).toBe( + RampsOrderStatus.Pending, + ); + expect(controller.state.orders[0]?.providerOrderId).toBe( + 'internal-order-123', + ); + }); + }); + it('uses wallet param when updating existing order and API omits walletAddress', async () => { await withController(async ({ controller, rootMessenger }) => { const existingOrder = createMockOrder({ @@ -7509,6 +7572,7 @@ describe('RampsController', () => { it('publishes orderStatusChanged when order status transitions', async () => { await withController(async ({ rootMessenger, messenger }) => { const pendingOrder = createMockOrder({ + id: '/providers/transak/orders/status-change-1', providerOrderId: 'status-change-1', status: RampsOrderStatus.Pending, provider: createMockProvider({ @@ -7538,7 +7602,10 @@ describe('RampsController', () => { await jest.advanceTimersByTimeAsync(0); expect(statusChangedListener).toHaveBeenCalledWith({ - order: updatedOrder, + order: { + ...updatedOrder, + providerOrderId: 'status-change-1', + }, previousStatus: RampsOrderStatus.Pending, }); @@ -7701,6 +7768,7 @@ describe('RampsController', () => { it('skips orders without providerOrderId', async () => { await withController(async ({ rootMessenger }) => { const orderNoId = createMockOrder({ + id: undefined, providerOrderId: '', status: RampsOrderStatus.Pending, provider: createMockProvider({ @@ -7751,6 +7819,7 @@ describe('RampsController', () => { it('passes provider id through to service without stripping prefix', async () => { await withController(async ({ rootMessenger }) => { const order = createMockOrder({ + id: '/providers/transak/orders/strip-prefix-1', providerOrderId: 'strip-prefix-1', status: RampsOrderStatus.Pending, provider: createMockProvider({ diff --git a/packages/ramps-controller/src/RampsController.ts b/packages/ramps-controller/src/RampsController.ts index 5993c6b4c9..d8c0371023 100644 --- a/packages/ramps-controller/src/RampsController.ts +++ b/packages/ramps-controller/src/RampsController.ts @@ -739,6 +739,31 @@ export function normalizeProviderCode(providerCode: string): string { return providerCode.replace(/^\/providers\//u, ''); } +/** + * Returns the internal MetaMask order code used for state lookups and polling. + * Prefers the code embedded in the canonical order `id` path over `providerOrderId`, + * which may contain the provider's native order identifier. + * + * @param orderOrId - Order fields or a full order id / order code string. + * @returns The internal order code. + */ +export function getInternalOrderCode( + orderOrId: Pick | string, +): string { + if (typeof orderOrId === 'string') { + return orderOrId.includes('/orders/') + ? orderOrId.split('/orders/')[1] + : orderOrId; + } + + const { id, providerOrderId } = orderOrId; + if (id?.includes('/orders/')) { + return id.split('/orders/')[1]; + } + + return providerOrderId; +} + // === ORDER POLLING CONSTANTS === const TERMINAL_ORDER_STATUSES = new Set([ @@ -2088,23 +2113,29 @@ export class RampsController extends BaseController< /** * Adds or updates a V2 order in controller state. - * If an order with the same providerOrderId already exists, the incoming + * If an order with the same internal order code already exists, the incoming * fields are merged on top of the existing order so that fields not present * in the update (e.g. paymentDetails from the Transak API) are preserved. * * @param order - The RampsOrder to add or update. */ addOrder(order: RampsOrder): void { + const internalOrderCode = getInternalOrderCode(order); + const healedOrder = { + ...order, + providerOrderId: internalOrderCode, + }; + this.update((state) => { const idx = state.orders.findIndex( - (existing) => existing.providerOrderId === order.providerOrderId, + (existing) => getInternalOrderCode(existing) === internalOrderCode, ); if (idx === -1) { - state.orders.push(order as Draft); + state.orders.push(healedOrder as Draft); } else { state.orders[idx] = { ...state.orders[idx], - ...order, + ...healedOrder, } as Draft; } }); @@ -2306,9 +2337,7 @@ export class RampsController extends BaseController< }): void { const { orderId, providerCode, walletAddress, chainId } = params; - const orderCode = orderId.includes('/orders/') - ? orderId.split('/orders/')[1] - : orderId; + const orderCode = getInternalOrderCode(orderId); if (!orderCode?.trim()) { return; } @@ -2368,15 +2397,20 @@ export class RampsController extends BaseController< ); const healedWalletAddress = order.walletAddress || wallet; + const internalOrderCode = getInternalOrderCode({ + id: order.id, + providerOrderId: orderCode, + }); const healedOrder = { ...order, walletAddress: healedWalletAddress, - providerOrderId: orderCode, + providerOrderId: internalOrderCode, }; this.update((state) => { const idx = state.orders.findIndex( - (existing: RampsOrder) => existing.providerOrderId === orderCode, + (existing: RampsOrder) => + getInternalOrderCode(existing) === internalOrderCode, ); if (idx === -1) { state.orders.push(healedOrder as Draft); diff --git a/packages/ramps-controller/src/index.ts b/packages/ramps-controller/src/index.ts index c1ec4d850c..7cc2d69e2a 100644 --- a/packages/ramps-controller/src/index.ts +++ b/packages/ramps-controller/src/index.ts @@ -64,6 +64,7 @@ export type { export { RampsController, getDefaultRampsControllerState, + getInternalOrderCode, normalizeProviderCode, RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS, } from './RampsController';