@payloadcms/plugin-ecommerce 4.0.0-canary.33 → 4.0.0-canary.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +6 -0
  2. package/dist/collections/orders/createOrdersCollection.spec.js +20 -0
  3. package/dist/collections/orders/createOrdersCollection.spec.js.map +1 -0
  4. package/dist/endpoints/confirmOrder.d.ts.map +1 -1
  5. package/dist/endpoints/confirmOrder.js +299 -44
  6. package/dist/endpoints/confirmOrder.js.map +1 -1
  7. package/dist/endpoints/confirmOrder.spec.js +509 -0
  8. package/dist/endpoints/confirmOrder.spec.js.map +1 -0
  9. package/dist/fields/statusField.d.ts.map +1 -1
  10. package/dist/fields/statusField.js +5 -0
  11. package/dist/fields/statusField.js.map +1 -1
  12. package/dist/fields/statusField.spec.js +9 -0
  13. package/dist/fields/statusField.spec.js.map +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/payments/adapters/stripe/confirmOrder.d.ts.map +1 -1
  18. package/dist/payments/adapters/stripe/confirmOrder.js +107 -75
  19. package/dist/payments/adapters/stripe/confirmOrder.js.map +1 -1
  20. package/dist/payments/adapters/stripe/confirmOrder.spec.js +210 -121
  21. package/dist/payments/adapters/stripe/confirmOrder.spec.js.map +1 -1
  22. package/dist/payments/adapters/stripe/validateSettlement.d.ts +37 -0
  23. package/dist/payments/adapters/stripe/validateSettlement.d.ts.map +1 -0
  24. package/dist/payments/adapters/stripe/validateSettlement.js +157 -0
  25. package/dist/payments/adapters/stripe/validateSettlement.js.map +1 -0
  26. package/dist/payments/adapters/stripe/validateSettlement.spec.js +434 -0
  27. package/dist/payments/adapters/stripe/validateSettlement.spec.js.map +1 -0
  28. package/dist/types/index.d.ts +24 -5
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/types/index.js.map +1 -1
  31. package/package.json +7 -7
@@ -1,14 +1,8 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- const mockCustomersList = vi.fn();
3
- const mockCustomersCreate = vi.fn();
4
2
  const mockPaymentIntentsRetrieve = vi.fn();
5
3
  vi.mock('stripe', ()=>{
6
4
  const MockStripe = function() {
7
5
  return {
8
- customers: {
9
- list: mockCustomersList,
10
- create: mockCustomersCreate
11
- },
12
6
  paymentIntents: {
13
7
  retrieve: mockPaymentIntentsRetrieve
14
8
  }
@@ -18,148 +12,243 @@ vi.mock('stripe', ()=>{
18
12
  default: MockStripe
19
13
  };
20
14
  });
21
- import { confirmOrder } from './confirmOrder';
22
- const defaultCartItemsSnapshot = JSON.stringify([
23
- {
24
- id: 'item-1',
25
- quantity: 1
26
- }
27
- ]);
28
- const createMockPaymentIntent = (status)=>({
15
+ import { confirmOrder } from './confirmOrder.js';
16
+ const createTransaction = (overrides = {})=>({
17
+ amount: 1000,
18
+ cart: 'cart-123',
19
+ currency: 'USD',
20
+ customer: 'user-123',
21
+ id: 'txn-123',
22
+ items: [
23
+ {
24
+ id: 'item-1',
25
+ product: 'product-123',
26
+ quantity: 1
27
+ }
28
+ ],
29
+ order: undefined,
30
+ status: 'pending',
31
+ stripe: {
32
+ customerID: 'cus-123',
33
+ paymentIntentID: 'pi_123'
34
+ },
35
+ ...overrides
36
+ });
37
+ const createPaymentIntent = (overrides = {})=>({
29
38
  amount: 1000,
30
39
  currency: 'usd',
40
+ customer: 'cus-123',
41
+ id: 'pi_123',
31
42
  metadata: {
32
43
  cartID: 'cart-123',
33
- cartItemsSnapshot: defaultCartItemsSnapshot,
44
+ cartItemsSnapshot: JSON.stringify([
45
+ {
46
+ id: 'item-1',
47
+ product: 'product-123',
48
+ quantity: 1
49
+ }
50
+ ]),
34
51
  shippingAddress: JSON.stringify({
35
52
  city: 'Test City'
36
53
  })
37
54
  },
38
- status
55
+ status: 'succeeded',
56
+ ...overrides
39
57
  });
40
- const createMockPayload = ()=>({
41
- create: vi.fn().mockResolvedValue({
42
- id: 'order-123'
43
- }),
44
- find: vi.fn().mockResolvedValue({
45
- docs: [
46
- {
47
- id: 'txn-123'
48
- }
49
- ],
50
- totalDocs: 1
51
- }),
52
- logger: {
53
- error: vi.fn()
54
- },
55
- update: vi.fn().mockResolvedValue({})
58
+ const createHarness = ({ transaction = createTransaction(), transactionCopies = 1, user = {
59
+ id: 'user-123'
60
+ } } = {})=>{
61
+ const find = vi.fn().mockResolvedValue({
62
+ docs: Array.from({
63
+ length: transactionCopies
64
+ }, ()=>transaction),
65
+ totalDocs: transactionCopies
56
66
  });
57
- const createMockReq = (payload)=>({
58
- payload,
59
- user: {
60
- id: 'user-123'
61
- }
67
+ const loggerError = vi.fn();
68
+ const finalizeOrder = vi.fn().mockResolvedValue({
69
+ accessToken: 'access-123',
70
+ id: 'order-123'
71
+ });
72
+ const req = {
73
+ payload: {
74
+ find,
75
+ logger: {
76
+ error: loggerError
77
+ }
78
+ },
79
+ user
80
+ };
81
+ return {
82
+ finalizeOrder,
83
+ find,
84
+ loggerError,
85
+ req
86
+ };
87
+ };
88
+ const runConfirmation = ({ data = {}, harness })=>confirmOrder({
89
+ secretKey: 'sk_test_123'
90
+ })({
91
+ data: {
92
+ cartID: 'cart-123',
93
+ customerEmail: 'buyer@example.com',
94
+ paymentIntentID: 'pi_123',
95
+ ...data
96
+ },
97
+ finalizeOrder: harness.finalizeOrder,
98
+ req: harness.req
62
99
  });
63
- describe('confirmOrder - payment status check', ()=>{
64
- const secretKey = 'sk_test_123';
100
+ describe('confirmOrder', ()=>{
65
101
  beforeEach(()=>{
66
102
  vi.clearAllMocks();
67
- mockCustomersList.mockResolvedValue({
68
- data: [
69
- {
70
- id: 'cus-123'
71
- }
72
- ]
73
- });
74
- mockCustomersCreate.mockResolvedValue({
75
- id: 'cus-new'
76
- });
103
+ mockPaymentIntentsRetrieve.mockResolvedValue(createPaymentIntent());
77
104
  });
78
- it('should throw when paymentIntent status is requires_payment_method', async ()=>{
79
- mockPaymentIntentsRetrieve.mockResolvedValue(createMockPaymentIntent('requires_payment_method'));
80
- const mockPayload = createMockPayload();
81
- const handler = confirmOrder({
82
- secretKey
105
+ it('should delegate provider-validated settlement data to the core finalizer', async ()=>{
106
+ const harness = createHarness();
107
+ const result = await runConfirmation({
108
+ harness
83
109
  });
84
- await expect(handler({
85
- data: {
86
- customerEmail: 'test@test.com',
87
- paymentIntentID: 'pi_123'
110
+ expect(harness.finalizeOrder).toHaveBeenCalledWith({
111
+ orderData: {
112
+ amount: 1000,
113
+ currency: 'USD',
114
+ customer: 'user-123',
115
+ items: [
116
+ {
117
+ id: 'item-1',
118
+ product: 'product-123',
119
+ quantity: 1
120
+ }
121
+ ],
122
+ shippingAddress: {
123
+ city: 'Test City'
124
+ },
125
+ status: 'processing'
88
126
  },
89
- req: createMockReq(mockPayload)
90
- })).rejects.toThrow('Payment not completed.');
91
- expect(mockPayload.create).not.toHaveBeenCalled();
127
+ transactionID: 'txn-123'
128
+ });
129
+ expect(result).toEqual({
130
+ accessToken: 'access-123',
131
+ message: 'Payment initiated successfully',
132
+ orderID: 'order-123',
133
+ transactionID: 'txn-123'
134
+ });
92
135
  });
93
- it('should throw when paymentIntent status is canceled', async ()=>{
94
- mockPaymentIntentsRetrieve.mockResolvedValue(createMockPaymentIntent('canceled'));
95
- const mockPayload = createMockPayload();
96
- const handler = confirmOrder({
97
- secretKey
136
+ it('should return the transaction ID again when the core finalizer returns an existing order', async ()=>{
137
+ const harness = createHarness();
138
+ harness.finalizeOrder.mockResolvedValue({
139
+ accessToken: 'existing-access',
140
+ id: 'existing-order'
141
+ });
142
+ const result = await runConfirmation({
143
+ harness
144
+ });
145
+ expect(result).toEqual({
146
+ accessToken: 'existing-access',
147
+ message: 'Payment initiated successfully',
148
+ orderID: 'existing-order',
149
+ transactionID: 'txn-123'
98
150
  });
99
- await expect(handler({
100
- data: {
101
- customerEmail: 'test@test.com',
102
- paymentIntentID: 'pi_123'
103
- },
104
- req: createMockReq(mockPayload)
105
- })).rejects.toThrow('Payment not completed.');
106
- expect(mockPayload.create).not.toHaveBeenCalled();
107
151
  });
108
- it('should throw when paymentIntent status is processing', async ()=>{
109
- mockPaymentIntentsRetrieve.mockResolvedValue(createMockPaymentIntent('processing'));
110
- const mockPayload = createMockPayload();
111
- const handler = confirmOrder({
112
- secretKey
152
+ it('should delegate guest order ownership using the validated email', async ()=>{
153
+ const harness = createHarness({
154
+ transaction: createTransaction({
155
+ customer: undefined,
156
+ customerEmail: 'buyer@example.com',
157
+ stripe: {
158
+ customerID: 'cus-guest',
159
+ paymentIntentID: 'pi_123'
160
+ }
161
+ }),
162
+ user: null
113
163
  });
114
- await expect(handler({
115
- data: {
116
- customerEmail: 'test@test.com',
117
- paymentIntentID: 'pi_123'
118
- },
119
- req: createMockReq(mockPayload)
164
+ mockPaymentIntentsRetrieve.mockResolvedValue(createPaymentIntent({
165
+ customer: 'cus-guest'
166
+ }));
167
+ await runConfirmation({
168
+ harness
169
+ });
170
+ expect(harness.finalizeOrder).toHaveBeenCalledWith({
171
+ orderData: expect.objectContaining({
172
+ customerEmail: 'buyer@example.com'
173
+ }),
174
+ transactionID: 'txn-123'
175
+ });
176
+ });
177
+ it.each([
178
+ 'requires_payment_method',
179
+ 'canceled',
180
+ 'processing'
181
+ ])('should reject a PaymentIntent with %s status before finalization', async (status)=>{
182
+ const harness = createHarness();
183
+ mockPaymentIntentsRetrieve.mockResolvedValue(createPaymentIntent({
184
+ status
185
+ }));
186
+ await expect(runConfirmation({
187
+ harness
120
188
  })).rejects.toThrow('Payment not completed.');
121
- expect(mockPayload.create).not.toHaveBeenCalled();
189
+ expect(harness.finalizeOrder).not.toHaveBeenCalled();
122
190
  });
123
- it('should not update cart or transaction when payment has not succeeded', async ()=>{
124
- mockPaymentIntentsRetrieve.mockResolvedValue(createMockPaymentIntent('requires_payment_method'));
125
- const mockPayload = createMockPayload();
126
- const handler = confirmOrder({
127
- secretKey
191
+ it('should reject multiple matching transactions before finalization', async ()=>{
192
+ const harness = createHarness({
193
+ transactionCopies: 2
128
194
  });
129
- await expect(handler({
130
- data: {
131
- customerEmail: 'test@test.com',
132
- paymentIntentID: 'pi_123'
133
- },
134
- req: createMockReq(mockPayload)
135
- })).rejects.toThrow();
136
- expect(mockPayload.update).not.toHaveBeenCalled();
195
+ await expect(runConfirmation({
196
+ harness
197
+ })).rejects.toThrow('exactly one transaction');
198
+ expect(mockPaymentIntentsRetrieve).not.toHaveBeenCalled();
199
+ expect(harness.finalizeOrder).not.toHaveBeenCalled();
137
200
  });
138
- it('should create order when paymentIntent status is succeeded', async ()=>{
139
- mockPaymentIntentsRetrieve.mockResolvedValue(createMockPaymentIntent('succeeded'));
140
- const mockPayload = createMockPayload();
141
- const handler = confirmOrder({
142
- secretKey
143
- });
144
- const result = await handler({
145
- data: {
146
- customerEmail: 'test@test.com',
147
- paymentIntentID: 'pi_123'
148
- },
149
- req: createMockReq(mockPayload)
150
- });
151
- expect(mockPayload.create).toHaveBeenCalledWith(expect.objectContaining({
152
- collection: 'orders',
153
- data: expect.objectContaining({
154
- amount: 1000,
155
- currency: 'USD',
156
- status: 'processing'
201
+ it('should reject a transaction that does not belong to the canonical cart', async ()=>{
202
+ const harness = createHarness({
203
+ transaction: createTransaction({
204
+ cart: 'cart-other'
157
205
  })
206
+ });
207
+ await expect(runConfirmation({
208
+ harness
209
+ })).rejects.toThrow('cart');
210
+ expect(harness.finalizeOrder).not.toHaveBeenCalled();
211
+ });
212
+ it('should reject provider amount drift before finalization', async ()=>{
213
+ const harness = createHarness();
214
+ mockPaymentIntentsRetrieve.mockResolvedValue(createPaymentIntent({
215
+ amount: 999
158
216
  }));
159
- expect(result).toEqual(expect.objectContaining({
160
- orderID: 'order-123',
161
- transactionID: 'txn-123'
217
+ await expect(runConfirmation({
218
+ harness
219
+ })).rejects.toThrow('amount');
220
+ expect(harness.finalizeOrder).not.toHaveBeenCalled();
221
+ });
222
+ it('should reject provider item drift before finalization', async ()=>{
223
+ const harness = createHarness();
224
+ mockPaymentIntentsRetrieve.mockResolvedValue(createPaymentIntent({
225
+ metadata: {
226
+ ...createPaymentIntent().metadata,
227
+ cartItemsSnapshot: JSON.stringify([
228
+ {
229
+ id: 'item-1',
230
+ product: 'product-123',
231
+ quantity: 2
232
+ }
233
+ ])
234
+ }
162
235
  }));
236
+ await expect(runConfirmation({
237
+ harness
238
+ })).rejects.toThrow('items');
239
+ expect(harness.finalizeOrder).not.toHaveBeenCalled();
240
+ });
241
+ it('should log and propagate finalization failures', async ()=>{
242
+ const harness = createHarness();
243
+ const error = new Error('inventory update failed');
244
+ harness.finalizeOrder.mockRejectedValue(error);
245
+ await expect(runConfirmation({
246
+ harness
247
+ })).rejects.toThrow(error.message);
248
+ expect(harness.loggerError).toHaveBeenCalledWith({
249
+ err: error,
250
+ msg: 'Error confirming order with Stripe'
251
+ });
163
252
  });
164
253
  });
165
254
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/payments/adapters/stripe/confirmOrder.spec.ts"],"sourcesContent":["import { beforeEach, describe, expect, it, vi } from 'vitest'\n\nconst mockCustomersList = vi.fn()\nconst mockCustomersCreate = vi.fn()\nconst mockPaymentIntentsRetrieve = vi.fn()\n\nvi.mock('stripe', () => {\n const MockStripe = function () {\n return {\n customers: {\n list: mockCustomersList,\n create: mockCustomersCreate,\n },\n paymentIntents: {\n retrieve: mockPaymentIntentsRetrieve,\n },\n }\n }\n\n return { default: MockStripe }\n})\n\nimport { confirmOrder } from './confirmOrder'\n\nconst defaultCartItemsSnapshot = JSON.stringify([{ id: 'item-1', quantity: 1 }])\n\nconst createMockPaymentIntent = (status: string) => ({\n amount: 1000,\n currency: 'usd',\n metadata: {\n cartID: 'cart-123',\n cartItemsSnapshot: defaultCartItemsSnapshot,\n shippingAddress: JSON.stringify({ city: 'Test City' }),\n },\n status,\n})\n\nconst createMockPayload = () => ({\n create: vi.fn().mockResolvedValue({ id: 'order-123' }),\n find: vi.fn().mockResolvedValue({\n docs: [{ id: 'txn-123' }],\n totalDocs: 1,\n }),\n logger: { error: vi.fn() },\n update: vi.fn().mockResolvedValue({}),\n})\n\nconst createMockReq = (payload: ReturnType<typeof createMockPayload>) =>\n ({\n payload,\n user: { id: 'user-123' },\n }) as any\n\ndescribe('confirmOrder - payment status check', () => {\n const secretKey = 'sk_test_123'\n\n beforeEach(() => {\n vi.clearAllMocks()\n\n mockCustomersList.mockResolvedValue({ data: [{ id: 'cus-123' }] })\n mockCustomersCreate.mockResolvedValue({ id: 'cus-new' })\n })\n\n it('should throw when paymentIntent status is requires_payment_method', async () => {\n mockPaymentIntentsRetrieve.mockResolvedValue(createMockPaymentIntent('requires_payment_method'))\n\n const mockPayload = createMockPayload()\n const handler = confirmOrder({ secretKey })\n\n await expect(\n handler({\n data: { customerEmail: 'test@test.com', paymentIntentID: 'pi_123' },\n req: createMockReq(mockPayload),\n }),\n ).rejects.toThrow('Payment not completed.')\n\n expect(mockPayload.create).not.toHaveBeenCalled()\n })\n\n it('should throw when paymentIntent status is canceled', async () => {\n mockPaymentIntentsRetrieve.mockResolvedValue(createMockPaymentIntent('canceled'))\n\n const mockPayload = createMockPayload()\n const handler = confirmOrder({ secretKey })\n\n await expect(\n handler({\n data: { customerEmail: 'test@test.com', paymentIntentID: 'pi_123' },\n req: createMockReq(mockPayload),\n }),\n ).rejects.toThrow('Payment not completed.')\n\n expect(mockPayload.create).not.toHaveBeenCalled()\n })\n\n it('should throw when paymentIntent status is processing', async () => {\n mockPaymentIntentsRetrieve.mockResolvedValue(createMockPaymentIntent('processing'))\n\n const mockPayload = createMockPayload()\n const handler = confirmOrder({ secretKey })\n\n await expect(\n handler({\n data: { customerEmail: 'test@test.com', paymentIntentID: 'pi_123' },\n req: createMockReq(mockPayload),\n }),\n ).rejects.toThrow('Payment not completed.')\n\n expect(mockPayload.create).not.toHaveBeenCalled()\n })\n\n it('should not update cart or transaction when payment has not succeeded', async () => {\n mockPaymentIntentsRetrieve.mockResolvedValue(createMockPaymentIntent('requires_payment_method'))\n\n const mockPayload = createMockPayload()\n const handler = confirmOrder({ secretKey })\n\n await expect(\n handler({\n data: { customerEmail: 'test@test.com', paymentIntentID: 'pi_123' },\n req: createMockReq(mockPayload),\n }),\n ).rejects.toThrow()\n\n expect(mockPayload.update).not.toHaveBeenCalled()\n })\n\n it('should create order when paymentIntent status is succeeded', async () => {\n mockPaymentIntentsRetrieve.mockResolvedValue(createMockPaymentIntent('succeeded'))\n\n const mockPayload = createMockPayload()\n const handler = confirmOrder({ secretKey })\n\n const result = await handler({\n data: { customerEmail: 'test@test.com', paymentIntentID: 'pi_123' },\n req: createMockReq(mockPayload),\n })\n\n expect(mockPayload.create).toHaveBeenCalledWith(\n expect.objectContaining({\n collection: 'orders',\n data: expect.objectContaining({\n amount: 1000,\n currency: 'USD',\n status: 'processing',\n }),\n }),\n )\n\n expect(result).toEqual(\n expect.objectContaining({\n orderID: 'order-123',\n transactionID: 'txn-123',\n }),\n )\n })\n})\n"],"names":["beforeEach","describe","expect","it","vi","mockCustomersList","fn","mockCustomersCreate","mockPaymentIntentsRetrieve","mock","MockStripe","customers","list","create","paymentIntents","retrieve","default","confirmOrder","defaultCartItemsSnapshot","JSON","stringify","id","quantity","createMockPaymentIntent","status","amount","currency","metadata","cartID","cartItemsSnapshot","shippingAddress","city","createMockPayload","mockResolvedValue","find","docs","totalDocs","logger","error","update","createMockReq","payload","user","secretKey","clearAllMocks","data","mockPayload","handler","customerEmail","paymentIntentID","req","rejects","toThrow","not","toHaveBeenCalled","result","toHaveBeenCalledWith","objectContaining","collection","toEqual","orderID","transactionID"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/payments/adapters/stripe/confirmOrder.spec.ts"],"sourcesContent":["import { beforeEach, describe, expect, it, vi } from 'vitest'\n\nconst mockPaymentIntentsRetrieve = vi.fn()\n\nvi.mock('stripe', () => {\n const MockStripe = function () {\n return {\n paymentIntents: {\n retrieve: mockPaymentIntentsRetrieve,\n },\n }\n }\n\n return { default: MockStripe }\n})\n\nimport { confirmOrder } from './confirmOrder.js'\n\ntype ConfirmOrderArgs = Parameters<ReturnType<typeof confirmOrder>>[0]\n\nconst createTransaction = (overrides: Record<string, unknown> = {}) => ({\n amount: 1000,\n cart: 'cart-123',\n currency: 'USD',\n customer: 'user-123',\n id: 'txn-123',\n items: [{ id: 'item-1', product: 'product-123', quantity: 1 }],\n order: undefined,\n status: 'pending',\n stripe: {\n customerID: 'cus-123',\n paymentIntentID: 'pi_123',\n },\n ...overrides,\n})\n\nconst createPaymentIntent = (overrides: Record<string, unknown> = {}) => ({\n amount: 1000,\n currency: 'usd',\n customer: 'cus-123',\n id: 'pi_123',\n metadata: {\n cartID: 'cart-123',\n cartItemsSnapshot: JSON.stringify([{ id: 'item-1', product: 'product-123', quantity: 1 }]),\n shippingAddress: JSON.stringify({ city: 'Test City' }),\n },\n status: 'succeeded',\n ...overrides,\n})\n\nconst createHarness = ({\n transaction = createTransaction(),\n transactionCopies = 1,\n user = { id: 'user-123' } as null | Record<string, unknown>,\n}: {\n transaction?: Record<string, unknown>\n transactionCopies?: number\n user?: null | Record<string, unknown>\n} = {}) => {\n const find = vi.fn().mockResolvedValue({\n docs: Array.from({ length: transactionCopies }, () => transaction),\n totalDocs: transactionCopies,\n })\n const loggerError = vi.fn()\n const finalizeOrder = vi.fn().mockResolvedValue({ accessToken: 'access-123', id: 'order-123' })\n const req = {\n payload: {\n find,\n logger: { error: loggerError },\n },\n user,\n } as unknown as ConfirmOrderArgs['req']\n\n return { finalizeOrder, find, loggerError, req }\n}\n\nconst runConfirmation = ({\n data = {},\n harness,\n}: {\n data?: Record<string, unknown>\n harness: ReturnType<typeof createHarness>\n}) =>\n confirmOrder({ secretKey: 'sk_test_123' })({\n data: {\n cartID: 'cart-123',\n customerEmail: 'buyer@example.com',\n paymentIntentID: 'pi_123',\n ...data,\n },\n finalizeOrder: harness.finalizeOrder,\n req: harness.req,\n })\n\ndescribe('confirmOrder', () => {\n beforeEach(() => {\n vi.clearAllMocks()\n mockPaymentIntentsRetrieve.mockResolvedValue(createPaymentIntent())\n })\n\n it('should delegate provider-validated settlement data to the core finalizer', async () => {\n const harness = createHarness()\n\n const result = await runConfirmation({ harness })\n\n expect(harness.finalizeOrder).toHaveBeenCalledWith({\n orderData: {\n amount: 1000,\n currency: 'USD',\n customer: 'user-123',\n items: [{ id: 'item-1', product: 'product-123', quantity: 1 }],\n shippingAddress: { city: 'Test City' },\n status: 'processing',\n },\n transactionID: 'txn-123',\n })\n expect(result).toEqual({\n accessToken: 'access-123',\n message: 'Payment initiated successfully',\n orderID: 'order-123',\n transactionID: 'txn-123',\n })\n })\n\n it('should return the transaction ID again when the core finalizer returns an existing order', async () => {\n const harness = createHarness()\n harness.finalizeOrder.mockResolvedValue({\n accessToken: 'existing-access',\n id: 'existing-order',\n })\n\n const result = await runConfirmation({ harness })\n\n expect(result).toEqual({\n accessToken: 'existing-access',\n message: 'Payment initiated successfully',\n orderID: 'existing-order',\n transactionID: 'txn-123',\n })\n })\n\n it('should delegate guest order ownership using the validated email', async () => {\n const harness = createHarness({\n transaction: createTransaction({\n customer: undefined,\n customerEmail: 'buyer@example.com',\n stripe: {\n customerID: 'cus-guest',\n paymentIntentID: 'pi_123',\n },\n }),\n user: null,\n })\n mockPaymentIntentsRetrieve.mockResolvedValue(createPaymentIntent({ customer: 'cus-guest' }))\n\n await runConfirmation({ harness })\n\n expect(harness.finalizeOrder).toHaveBeenCalledWith({\n orderData: expect.objectContaining({ customerEmail: 'buyer@example.com' }),\n transactionID: 'txn-123',\n })\n })\n\n it.each(['requires_payment_method', 'canceled', 'processing'])(\n 'should reject a PaymentIntent with %s status before finalization',\n async (status) => {\n const harness = createHarness()\n mockPaymentIntentsRetrieve.mockResolvedValue(createPaymentIntent({ status }))\n\n await expect(runConfirmation({ harness })).rejects.toThrow('Payment not completed.')\n\n expect(harness.finalizeOrder).not.toHaveBeenCalled()\n },\n )\n\n it('should reject multiple matching transactions before finalization', async () => {\n const harness = createHarness({ transactionCopies: 2 })\n\n await expect(runConfirmation({ harness })).rejects.toThrow('exactly one transaction')\n\n expect(mockPaymentIntentsRetrieve).not.toHaveBeenCalled()\n expect(harness.finalizeOrder).not.toHaveBeenCalled()\n })\n\n it('should reject a transaction that does not belong to the canonical cart', async () => {\n const harness = createHarness({\n transaction: createTransaction({ cart: 'cart-other' }),\n })\n\n await expect(runConfirmation({ harness })).rejects.toThrow('cart')\n\n expect(harness.finalizeOrder).not.toHaveBeenCalled()\n })\n\n it('should reject provider amount drift before finalization', async () => {\n const harness = createHarness()\n mockPaymentIntentsRetrieve.mockResolvedValue(createPaymentIntent({ amount: 999 }))\n\n await expect(runConfirmation({ harness })).rejects.toThrow('amount')\n\n expect(harness.finalizeOrder).not.toHaveBeenCalled()\n })\n\n it('should reject provider item drift before finalization', async () => {\n const harness = createHarness()\n mockPaymentIntentsRetrieve.mockResolvedValue(\n createPaymentIntent({\n metadata: {\n ...createPaymentIntent().metadata,\n cartItemsSnapshot: JSON.stringify([\n { id: 'item-1', product: 'product-123', quantity: 2 },\n ]),\n },\n }),\n )\n\n await expect(runConfirmation({ harness })).rejects.toThrow('items')\n\n expect(harness.finalizeOrder).not.toHaveBeenCalled()\n })\n\n it('should log and propagate finalization failures', async () => {\n const harness = createHarness()\n const error = new Error('inventory update failed')\n harness.finalizeOrder.mockRejectedValue(error)\n\n await expect(runConfirmation({ harness })).rejects.toThrow(error.message)\n\n expect(harness.loggerError).toHaveBeenCalledWith({\n err: error,\n msg: 'Error confirming order with Stripe',\n })\n })\n})\n"],"names":["beforeEach","describe","expect","it","vi","mockPaymentIntentsRetrieve","fn","mock","MockStripe","paymentIntents","retrieve","default","confirmOrder","createTransaction","overrides","amount","cart","currency","customer","id","items","product","quantity","order","undefined","status","stripe","customerID","paymentIntentID","createPaymentIntent","metadata","cartID","cartItemsSnapshot","JSON","stringify","shippingAddress","city","createHarness","transaction","transactionCopies","user","find","mockResolvedValue","docs","Array","from","length","totalDocs","loggerError","finalizeOrder","accessToken","req","payload","logger","error","runConfirmation","data","harness","secretKey","customerEmail","clearAllMocks","result","toHaveBeenCalledWith","orderData","transactionID","toEqual","message","orderID","objectContaining","each","rejects","toThrow","not","toHaveBeenCalled","Error","mockRejectedValue","err","msg"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ type SettlementPaymentIntent = {
2
+ amount?: unknown;
3
+ currency?: unknown;
4
+ customer?: unknown;
5
+ id?: unknown;
6
+ metadata?: unknown;
7
+ status?: unknown;
8
+ };
9
+ type SettlementTransaction = {
10
+ amount?: unknown;
11
+ cart?: unknown;
12
+ currency?: unknown;
13
+ customer?: unknown;
14
+ customerEmail?: unknown;
15
+ id?: unknown;
16
+ items?: unknown;
17
+ order?: unknown;
18
+ status?: unknown;
19
+ stripe?: unknown;
20
+ };
21
+ type SettlementValidationArgs = {
22
+ canonicalCartID: unknown;
23
+ cartItemsSnapshot: unknown;
24
+ customerEmail: unknown;
25
+ paymentIntent: SettlementPaymentIntent;
26
+ paymentIntentID: string;
27
+ transaction: SettlementTransaction;
28
+ user?: {
29
+ id?: unknown;
30
+ } | null;
31
+ };
32
+ export declare const getRelationshipID: (value: unknown) => string | undefined;
33
+ export declare const normalizeEmail: (value: unknown) => string | undefined;
34
+ export declare const normalizeCartItems: (items: unknown) => unknown[];
35
+ export declare const validateSettlement: ({ canonicalCartID, cartItemsSnapshot, customerEmail, paymentIntent, paymentIntentID, transaction, user, }: SettlementValidationArgs) => void;
36
+ export {};
37
+ //# sourceMappingURL=validateSettlement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateSettlement.d.ts","sourceRoot":"","sources":["../../../../src/payments/adapters/stripe/validateSettlement.ts"],"names":[],"mappings":"AAEA,KAAK,uBAAuB,GAAG;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,KAAK,qBAAqB,GAAG;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,KAAK,wBAAwB,GAAG;IAC9B,eAAe,EAAE,OAAO,CAAA;IACxB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,aAAa,EAAE,OAAO,CAAA;IACtB,aAAa,EAAE,uBAAuB,CAAA;IACtC,eAAe,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,qBAAqB,CAAA;IAClC,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAA;CAC/B,CAAA;AAED,eAAO,MAAM,iBAAiB,UAAW,OAAO,KAAG,MAAM,GAAG,SAY3D,CAAA;AAED,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,MAAM,GAAG,SAQxD,CAAA;AAED,eAAO,MAAM,kBAAkB,UAAW,OAAO,KAAG,OAAO,EA0C1D,CAAA;AAED,eAAO,MAAM,kBAAkB,8GAQ5B,wBAAwB,KAAG,IA2E7B,CAAA"}
@@ -0,0 +1,157 @@
1
+ export const getRelationshipID = (value)=>{
2
+ const relationshipID = isRecord(value) ? value.id : value;
3
+ if (typeof relationshipID === 'number' && Number.isFinite(relationshipID)) {
4
+ return String(relationshipID);
5
+ }
6
+ if (typeof relationshipID === 'string' && relationshipID.trim()) {
7
+ return relationshipID;
8
+ }
9
+ return undefined;
10
+ };
11
+ export const normalizeEmail = (value)=>{
12
+ if (typeof value !== 'string') {
13
+ return undefined;
14
+ }
15
+ const normalizedEmail = value.trim().toLowerCase();
16
+ return normalizedEmail || undefined;
17
+ };
18
+ export const normalizeCartItems = (items)=>{
19
+ if (!Array.isArray(items) || items.length === 0) {
20
+ throw new Error('Cart items are missing or invalid');
21
+ }
22
+ return items.map((item)=>{
23
+ if (!isRecord(item)) {
24
+ throw new Error('Cart items are missing or invalid');
25
+ }
26
+ const productID = getRelationshipID(item.product);
27
+ if (!productID || typeof item.quantity !== 'number' || !Number.isFinite(item.quantity)) {
28
+ throw new Error('Cart items are missing or invalid');
29
+ }
30
+ return Object.fromEntries(Object.entries(item).flatMap(([key, value])=>{
31
+ if (key === 'product') {
32
+ return [
33
+ [
34
+ key,
35
+ productID
36
+ ]
37
+ ];
38
+ }
39
+ if (key === 'variant') {
40
+ if (value === null || value === undefined) {
41
+ return [];
42
+ }
43
+ const variantID = getRelationshipID(value);
44
+ if (!variantID) {
45
+ throw new Error('Cart item variant relationship is invalid');
46
+ }
47
+ return [
48
+ [
49
+ key,
50
+ variantID
51
+ ]
52
+ ];
53
+ }
54
+ const normalizedValue = normalizeValue(value);
55
+ return normalizedValue === undefined ? [] : [
56
+ [
57
+ key,
58
+ normalizedValue
59
+ ]
60
+ ];
61
+ }));
62
+ });
63
+ };
64
+ export const validateSettlement = ({ canonicalCartID, cartItemsSnapshot, customerEmail, paymentIntent, paymentIntentID, transaction, user })=>{
65
+ if (paymentIntentID !== getNestedString(transaction, 'stripe', 'paymentIntentID') || paymentIntentID !== paymentIntent.id) {
66
+ throw new Error('PaymentIntent does not match the transaction');
67
+ }
68
+ const normalizedCartID = getRelationshipID(canonicalCartID);
69
+ if (!normalizedCartID || normalizedCartID !== getRelationshipID(transaction.cart)) {
70
+ throw new Error('Canonical cart does not match the transaction cart');
71
+ }
72
+ const metadata = isRecord(paymentIntent.metadata) ? paymentIntent.metadata : undefined;
73
+ if (normalizedCartID !== getRelationshipID(metadata?.cartID)) {
74
+ throw new Error('Canonical cart does not match the PaymentIntent cart');
75
+ }
76
+ const transactionCustomerID = getRelationshipID(transaction.customer);
77
+ if (user !== null && user !== undefined) {
78
+ const authenticatedUserID = getRelationshipID(user.id);
79
+ if (!authenticatedUserID) {
80
+ throw new Error('Authenticated customer ID is missing or invalid');
81
+ }
82
+ if (transactionCustomerID !== authenticatedUserID) {
83
+ throw new Error('Authenticated customer does not match the transaction customer');
84
+ }
85
+ } else {
86
+ if (transactionCustomerID) {
87
+ throw new Error('Guest transaction belongs to an authenticated customer');
88
+ }
89
+ const normalizedCustomerEmail = normalizeEmail(customerEmail);
90
+ if (!normalizedCustomerEmail || normalizedCustomerEmail !== normalizeEmail(transaction.customerEmail)) {
91
+ throw new Error('Guest email does not match the transaction email');
92
+ }
93
+ }
94
+ const stripeCustomerID = getNestedString(transaction, 'stripe', 'customerID');
95
+ if (!stripeCustomerID || stripeCustomerID !== getRelationshipID(paymentIntent.customer)) {
96
+ throw new Error('Stripe customer does not match the transaction');
97
+ }
98
+ if (paymentIntent.amount !== transaction.amount) {
99
+ throw new Error('Stripe amount does not match the transaction amount');
100
+ }
101
+ if (typeof paymentIntent.currency !== 'string' || typeof transaction.currency !== 'string' || paymentIntent.currency.toUpperCase() !== transaction.currency) {
102
+ throw new Error('Stripe currency does not match the transaction currency');
103
+ }
104
+ const normalizedSnapshot = normalizeCartItems(cartItemsSnapshot);
105
+ const normalizedTransactionItems = normalizeCartItems(transaction.items);
106
+ if (stableSerialize(normalizedSnapshot) !== stableSerialize(normalizedTransactionItems)) {
107
+ throw new Error('Stripe cart items do not match the transaction items');
108
+ }
109
+ if (paymentIntent.status !== 'succeeded') {
110
+ throw new Error('Payment not completed.');
111
+ }
112
+ };
113
+ const getNestedString = (value, groupName, fieldName)=>{
114
+ if (!isRecord(value)) {
115
+ return undefined;
116
+ }
117
+ const group = value[groupName];
118
+ if (!isRecord(group) || typeof group[fieldName] !== 'string') {
119
+ return undefined;
120
+ }
121
+ return group[fieldName];
122
+ };
123
+ const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
124
+ const normalizeValue = (value)=>{
125
+ if (Array.isArray(value)) {
126
+ return value.map((entry)=>normalizeValue(entry));
127
+ }
128
+ if (isRecord(value)) {
129
+ return Object.fromEntries(Object.entries(value).flatMap(([entryKey, entryValue])=>{
130
+ const normalizedValue = normalizeValue(entryValue);
131
+ return normalizedValue === undefined ? [] : [
132
+ [
133
+ entryKey,
134
+ normalizedValue
135
+ ]
136
+ ];
137
+ }));
138
+ }
139
+ if (value === null || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
140
+ return value;
141
+ }
142
+ if (value === undefined) {
143
+ return undefined;
144
+ }
145
+ throw new Error('Cart items contain unsupported data');
146
+ };
147
+ const stableSerialize = (value)=>{
148
+ if (Array.isArray(value)) {
149
+ return `[${value.map((entry)=>stableSerialize(entry)).join(',')}]`;
150
+ }
151
+ if (isRecord(value)) {
152
+ return `{${Object.keys(value).sort().map((key)=>`${JSON.stringify(key)}:${stableSerialize(value[key])}`).join(',')}}`;
153
+ }
154
+ return JSON.stringify(value);
155
+ };
156
+
157
+ //# sourceMappingURL=validateSettlement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/payments/adapters/stripe/validateSettlement.ts"],"sourcesContent":["type RecordValue = Record<string, unknown>\n\ntype SettlementPaymentIntent = {\n amount?: unknown\n currency?: unknown\n customer?: unknown\n id?: unknown\n metadata?: unknown\n status?: unknown\n}\n\ntype SettlementTransaction = {\n amount?: unknown\n cart?: unknown\n currency?: unknown\n customer?: unknown\n customerEmail?: unknown\n id?: unknown\n items?: unknown\n order?: unknown\n status?: unknown\n stripe?: unknown\n}\n\ntype SettlementValidationArgs = {\n canonicalCartID: unknown\n cartItemsSnapshot: unknown\n customerEmail: unknown\n paymentIntent: SettlementPaymentIntent\n paymentIntentID: string\n transaction: SettlementTransaction\n user?: { id?: unknown } | null\n}\n\nexport const getRelationshipID = (value: unknown): string | undefined => {\n const relationshipID = isRecord(value) ? value.id : value\n\n if (typeof relationshipID === 'number' && Number.isFinite(relationshipID)) {\n return String(relationshipID)\n }\n\n if (typeof relationshipID === 'string' && relationshipID.trim()) {\n return relationshipID\n }\n\n return undefined\n}\n\nexport const normalizeEmail = (value: unknown): string | undefined => {\n if (typeof value !== 'string') {\n return undefined\n }\n\n const normalizedEmail = value.trim().toLowerCase()\n\n return normalizedEmail || undefined\n}\n\nexport const normalizeCartItems = (items: unknown): unknown[] => {\n if (!Array.isArray(items) || items.length === 0) {\n throw new Error('Cart items are missing or invalid')\n }\n\n return items.map((item) => {\n if (!isRecord(item)) {\n throw new Error('Cart items are missing or invalid')\n }\n\n const productID = getRelationshipID(item.product)\n\n if (!productID || typeof item.quantity !== 'number' || !Number.isFinite(item.quantity)) {\n throw new Error('Cart items are missing or invalid')\n }\n\n return Object.fromEntries(\n Object.entries(item).flatMap(([key, value]) => {\n if (key === 'product') {\n return [[key, productID]]\n }\n\n if (key === 'variant') {\n if (value === null || value === undefined) {\n return []\n }\n\n const variantID = getRelationshipID(value)\n\n if (!variantID) {\n throw new Error('Cart item variant relationship is invalid')\n }\n\n return [[key, variantID]]\n }\n\n const normalizedValue = normalizeValue(value)\n\n return normalizedValue === undefined ? [] : [[key, normalizedValue]]\n }),\n )\n })\n}\n\nexport const validateSettlement = ({\n canonicalCartID,\n cartItemsSnapshot,\n customerEmail,\n paymentIntent,\n paymentIntentID,\n transaction,\n user,\n}: SettlementValidationArgs): void => {\n if (\n paymentIntentID !== getNestedString(transaction, 'stripe', 'paymentIntentID') ||\n paymentIntentID !== paymentIntent.id\n ) {\n throw new Error('PaymentIntent does not match the transaction')\n }\n\n const normalizedCartID = getRelationshipID(canonicalCartID)\n\n if (!normalizedCartID || normalizedCartID !== getRelationshipID(transaction.cart)) {\n throw new Error('Canonical cart does not match the transaction cart')\n }\n\n const metadata = isRecord(paymentIntent.metadata) ? paymentIntent.metadata : undefined\n\n if (normalizedCartID !== getRelationshipID(metadata?.cartID)) {\n throw new Error('Canonical cart does not match the PaymentIntent cart')\n }\n\n const transactionCustomerID = getRelationshipID(transaction.customer)\n\n if (user !== null && user !== undefined) {\n const authenticatedUserID = getRelationshipID(user.id)\n\n if (!authenticatedUserID) {\n throw new Error('Authenticated customer ID is missing or invalid')\n }\n\n if (transactionCustomerID !== authenticatedUserID) {\n throw new Error('Authenticated customer does not match the transaction customer')\n }\n } else {\n if (transactionCustomerID) {\n throw new Error('Guest transaction belongs to an authenticated customer')\n }\n\n const normalizedCustomerEmail = normalizeEmail(customerEmail)\n\n if (\n !normalizedCustomerEmail ||\n normalizedCustomerEmail !== normalizeEmail(transaction.customerEmail)\n ) {\n throw new Error('Guest email does not match the transaction email')\n }\n }\n\n const stripeCustomerID = getNestedString(transaction, 'stripe', 'customerID')\n\n if (!stripeCustomerID || stripeCustomerID !== getRelationshipID(paymentIntent.customer)) {\n throw new Error('Stripe customer does not match the transaction')\n }\n\n if (paymentIntent.amount !== transaction.amount) {\n throw new Error('Stripe amount does not match the transaction amount')\n }\n\n if (\n typeof paymentIntent.currency !== 'string' ||\n typeof transaction.currency !== 'string' ||\n paymentIntent.currency.toUpperCase() !== transaction.currency\n ) {\n throw new Error('Stripe currency does not match the transaction currency')\n }\n\n const normalizedSnapshot = normalizeCartItems(cartItemsSnapshot)\n const normalizedTransactionItems = normalizeCartItems(transaction.items)\n\n if (stableSerialize(normalizedSnapshot) !== stableSerialize(normalizedTransactionItems)) {\n throw new Error('Stripe cart items do not match the transaction items')\n }\n\n if (paymentIntent.status !== 'succeeded') {\n throw new Error('Payment not completed.')\n }\n}\n\nconst getNestedString = (\n value: unknown,\n groupName: string,\n fieldName: string,\n): string | undefined => {\n if (!isRecord(value)) {\n return undefined\n }\n\n const group = value[groupName]\n\n if (!isRecord(group) || typeof group[fieldName] !== 'string') {\n return undefined\n }\n\n return group[fieldName]\n}\n\nconst isRecord = (value: unknown): value is RecordValue =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst normalizeValue = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map((entry) => normalizeValue(entry))\n }\n\n if (isRecord(value)) {\n return Object.fromEntries(\n Object.entries(value).flatMap(([entryKey, entryValue]) => {\n const normalizedValue = normalizeValue(entryValue)\n\n return normalizedValue === undefined ? [] : [[entryKey, normalizedValue]]\n }),\n )\n }\n\n if (\n value === null ||\n typeof value === 'boolean' ||\n typeof value === 'number' ||\n typeof value === 'string'\n ) {\n return value\n }\n\n if (value === undefined) {\n return undefined\n }\n\n throw new Error('Cart items contain unsupported data')\n}\n\nconst stableSerialize = (value: unknown): string => {\n if (Array.isArray(value)) {\n return `[${value.map((entry) => stableSerialize(entry)).join(',')}]`\n }\n\n if (isRecord(value)) {\n return `{${Object.keys(value)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)\n .join(',')}}`\n }\n\n return JSON.stringify(value)\n}\n"],"names":["getRelationshipID","value","relationshipID","isRecord","id","Number","isFinite","String","trim","undefined","normalizeEmail","normalizedEmail","toLowerCase","normalizeCartItems","items","Array","isArray","length","Error","map","item","productID","product","quantity","Object","fromEntries","entries","flatMap","key","variantID","normalizedValue","normalizeValue","validateSettlement","canonicalCartID","cartItemsSnapshot","customerEmail","paymentIntent","paymentIntentID","transaction","user","getNestedString","normalizedCartID","cart","metadata","cartID","transactionCustomerID","customer","authenticatedUserID","normalizedCustomerEmail","stripeCustomerID","amount","currency","toUpperCase","normalizedSnapshot","normalizedTransactionItems","stableSerialize","status","groupName","fieldName","group","entry","entryKey","entryValue","join","keys","sort","JSON","stringify"],"mappings":"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"}