From b5706ab4979f8c018a573d18699a6f561b3afe5b Mon Sep 17 00:00:00 2001 From: Karan Narula Date: Fri, 11 Sep 2026 17:06:55 -0400 Subject: [PATCH 1/6] feat(stripe): record Tempo session settlements automatically Committed-By-Agent: codex Co-authored-by: codex --- .changeset/stripe-session-paymentintents.md | 5 + src/stripe/server/Methods.session.test.ts | 170 ++++++++++++++++++++ src/stripe/server/Methods.ts | 17 +- 3 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 .changeset/stripe-session-paymentintents.md create mode 100644 src/stripe/server/Methods.session.test.ts diff --git a/.changeset/stripe-session-paymentintents.md b/.changeset/stripe-session-paymentintents.md new file mode 100644 index 00000000..fc239fe9 --- /dev/null +++ b/.changeset/stripe-session-paymentintents.md @@ -0,0 +1,5 @@ +--- +'mppx': patch +--- + +Added automatic Stripe PaymentIntent recording for Tempo session settlements, using each transaction's newly settled amount rounded down to whole cents. Preserved optional settlement callbacks after recording; applications no longer need to create PaymentIntents in those callbacks. diff --git a/src/stripe/server/Methods.session.test.ts b/src/stripe/server/Methods.session.test.ts new file mode 100644 index 00000000..1d2972d3 --- /dev/null +++ b/src/stripe/server/Methods.session.test.ts @@ -0,0 +1,170 @@ +import { afterEach, describe, expect, test, vi } from 'vp/test' + +import { sdkIdentifier } from '../../internal/version.js' +import * as TempoSession from '../../tempo/session/server/Session.js' +import type { SessionSettlementContext } from '../../tempo/session/server/Settlement.js' +import { stripePreviewVersion } from '../internal/constants.js' +import type { StripeClient } from '../internal/types.js' +import { stripe } from './Methods.js' + +const recipient = '0x1111111111111111111111111111111111111111' as stripe.DepositAddress<'tempo'> +const context: SessionSettlementContext = { + txHash: `0x${'ab'.repeat(32)}`, + channelId: `0x${'cd'.repeat(32)}`, + trigger: 'scheduled', + amount: 50_000n, + delta: 10_000n, +} + +function setup( + options: { + additional?: boolean + onSessionSettlement?: TempoSession.session.Parameters['onSessionSettlement'] + } = {}, +) { + const create = vi.fn(async () => ({ + id: 'pi_test', + status: 'succeeded', + })) + const client: StripeClient = { + paymentIntents: { create }, + rawRequest: vi.fn(), + } + const payments = stripe({ + client, + networkId: 'test-profile', + livemode: true, + metadata: { integration: 'sessions' }, + connect: { stripeAccount: 'acct_test', applicationFeeAmount: 1 }, + depositAddresses: { tempo: recipient }, + }) + // Capture the hook passed to the real Tempo method; only Stripe I/O is mocked. + const session = vi.spyOn(TempoSession, 'session') + const parameters = { + settlementSchedule: { amount: '0.01' }, + onSessionSettlement: options.onSessionSettlement, + } + if (options.additional) payments.defaultMethods().additional({ tempo: { session: parameters } }) + else payments.tempo.session({ recipient, ...parameters }) + const settle = session.mock.calls.at(-1)![0]!.onSessionSettlement! + return { create, settle } +} + +afterEach(() => vi.restoreAllMocks()) + +describe('Stripe session settlement recording', () => { + test.each(['scheduled', 'settle', 'close'] as const)( + 'records the delta for a %s transaction through the shared Stripe recorder', + async (trigger) => { + const { create, settle } = setup() + await settle({ ...context, trigger }) + + expect(create).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + amount: 1, + currency: 'usd', + confirm: true, + payment_method_data: { type: 'crypto' }, + payment_method_types: ['crypto'], + payment_method_options: { + crypto: { + mode: 'transaction_verification', + transaction_verification_options: { + network: 'tempo', + transaction_hash: context.txHash, + }, + }, + }, + application_fee_amount: 1, + metadata: expect.objectContaining({ + mpp_sdk: sdkIdentifier, + integration: 'sessions', + }), + }), + { + apiVersion: stripePreviewVersion, + idempotencyKey: context.txHash, + stripeAccount: 'acct_test', + }, + ) + }, + ) + + test('also records sessions added to defaultMethods()', async () => { + const { create, settle } = setup({ additional: true }) + await settle(context) + expect(create).toHaveBeenCalledTimes(1) + }) + + test.each([0n, 1n, 5_000n, 9_999n])( + 'skips a %s raw-unit delta but calls the merchant hook', + async (delta) => { + const onSessionSettlement = vi.fn() + const { create, settle } = setup({ onSessionSettlement }) + const event = { ...context, trigger: 'close' as const, delta } + await settle(event) + expect(create).not.toHaveBeenCalled() + expect(onSessionSettlement).toHaveBeenCalledExactlyOnceWith(event) + }, + ) + + test('floors each transaction independently without carrying fractional cents', async () => { + const { create, settle } = setup() + await settle({ ...context, delta: 19_999n }) + await settle({ ...context, txHash: `0x${'ef'.repeat(32)}`, amount: 69_998n, delta: 19_999n }) + expect(create.mock.calls.map(([params]) => (params as any).amount)).toEqual([1, 1]) + }) + + test('uses the same idempotency key when a settlement is delivered again', async () => { + const { create, settle } = setup() + await settle(context) + await settle(context) + expect(create.mock.calls[0]).toEqual(create.mock.calls[1]) + }) + + test('records before calling the merchant hook', async () => { + const onSessionSettlement = vi.fn(() => { + expect(create).toHaveBeenCalledTimes(1) + }) + const { create, settle } = setup({ onSessionSettlement }) + await settle(context) + expect(onSessionSettlement).toHaveBeenCalledExactlyOnceWith(context) + }) + + test('a merchant hook failure does not prevent recording', async () => { + const error = new Error('merchant hook failed') + const { create, settle } = setup({ + onSessionSettlement: () => { + throw error + }, + }) + await expect(settle(context)).rejects.toThrow(error) + expect(create).toHaveBeenCalledTimes(1) + }) + + test('logs Stripe failures without failing settlement or skipping the merchant hook', async () => { + const onSessionSettlement = vi.fn() + const { create, settle } = setup({ onSessionSettlement }) + const error = new Error('Stripe unavailable') + create.mockRejectedValueOnce(error) + const log = vi.spyOn(console, 'error').mockImplementation(() => {}) + await expect(settle(context)).resolves.toBeUndefined() + expect(log).toHaveBeenCalledWith('[stripe] failed to record crypto payment:', error) + expect(onSessionSettlement).toHaveBeenCalledExactlyOnceWith(context) + }) + + test('inherits the shared recorder fallback when Stripe rejects optional metadata', async () => { + const { create, settle } = setup() + create.mockRejectedValueOnce({ type: 'StripeInvalidRequestError' }) + vi.spyOn(console, 'warn').mockImplementation(() => {}) + await settle(context) + expect(create).toHaveBeenCalledTimes(2) + expect(create).toHaveBeenLastCalledWith( + expect.objectContaining({ + amount: 1, + metadata: { machine_payment: 'true', mpp_sdk: sdkIdentifier }, + }), + expect.objectContaining({ idempotencyKey: `${context.txHash}_fallback` }), + ) + }) +}) diff --git a/src/stripe/server/Methods.ts b/src/stripe/server/Methods.ts index f5f7fff2..0df8c9b0 100644 --- a/src/stripe/server/Methods.ts +++ b/src/stripe/server/Methods.ts @@ -99,6 +99,10 @@ interface StripeMachinePayments

metadata?: Record } & Partial[0], 'currency' | 'recipient'>>, ) => TempoServer + /** + * Creates a session that automatically records whole-cent settlement deltas + * as Stripe PaymentIntents. `onSessionSettlement` runs after recording. + */ session: ( params: { recipient: stripe.DepositAddress<'tempo'> } & Omit< tempoSession.Parameters, @@ -256,13 +260,24 @@ export function stripe(parameters: P): Stripe function makeTempoSession( params: { recipient: `0x${string}` } & Omit, ): Method.AnyServer { - const { recipient, ...rest } = params + const { recipient, onSessionSettlement, ...rest } = params return tempoSession({ currency: tempoCurrency, recipient, ...(!livemode && { testnet: true }), ...(hostedTempoFeePayer && { feePayer: hostedTempoFeePayer }), ...rest, + async onSessionSettlement(context) { + // Stripe verifies each transaction in whole cents. Never round a + // settlement up or carry its sub-cent remainder into another transaction. + const amount = (context.delta / 10_000n) * 10_000n + if (amount > 0n) + await tempoPaymentHandler({ + receipt: { reference: context.txHash }, + request: { amount: amount.toString() }, + }) + await onSessionSettlement?.(context) + }, } as tempoSession.Parameters) as Method.AnyServer } From e7060a99bda5fafffb8665a669c53ab125947b66 Mon Sep 17 00:00:00 2001 From: Karan Narula Date: Mon, 14 Sep 2026 14:32:29 -0400 Subject: [PATCH 2/6] test(stripe): focus session coverage on settlement wiring Committed-By-Agent: codex Co-authored-by: codex --- src/stripe/server/Methods.session.test.ts | 170 ---------------------- src/stripe/server/Methods.test.ts | 73 ++++++++++ 2 files changed, 73 insertions(+), 170 deletions(-) delete mode 100644 src/stripe/server/Methods.session.test.ts diff --git a/src/stripe/server/Methods.session.test.ts b/src/stripe/server/Methods.session.test.ts deleted file mode 100644 index 1d2972d3..00000000 --- a/src/stripe/server/Methods.session.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { afterEach, describe, expect, test, vi } from 'vp/test' - -import { sdkIdentifier } from '../../internal/version.js' -import * as TempoSession from '../../tempo/session/server/Session.js' -import type { SessionSettlementContext } from '../../tempo/session/server/Settlement.js' -import { stripePreviewVersion } from '../internal/constants.js' -import type { StripeClient } from '../internal/types.js' -import { stripe } from './Methods.js' - -const recipient = '0x1111111111111111111111111111111111111111' as stripe.DepositAddress<'tempo'> -const context: SessionSettlementContext = { - txHash: `0x${'ab'.repeat(32)}`, - channelId: `0x${'cd'.repeat(32)}`, - trigger: 'scheduled', - amount: 50_000n, - delta: 10_000n, -} - -function setup( - options: { - additional?: boolean - onSessionSettlement?: TempoSession.session.Parameters['onSessionSettlement'] - } = {}, -) { - const create = vi.fn(async () => ({ - id: 'pi_test', - status: 'succeeded', - })) - const client: StripeClient = { - paymentIntents: { create }, - rawRequest: vi.fn(), - } - const payments = stripe({ - client, - networkId: 'test-profile', - livemode: true, - metadata: { integration: 'sessions' }, - connect: { stripeAccount: 'acct_test', applicationFeeAmount: 1 }, - depositAddresses: { tempo: recipient }, - }) - // Capture the hook passed to the real Tempo method; only Stripe I/O is mocked. - const session = vi.spyOn(TempoSession, 'session') - const parameters = { - settlementSchedule: { amount: '0.01' }, - onSessionSettlement: options.onSessionSettlement, - } - if (options.additional) payments.defaultMethods().additional({ tempo: { session: parameters } }) - else payments.tempo.session({ recipient, ...parameters }) - const settle = session.mock.calls.at(-1)![0]!.onSessionSettlement! - return { create, settle } -} - -afterEach(() => vi.restoreAllMocks()) - -describe('Stripe session settlement recording', () => { - test.each(['scheduled', 'settle', 'close'] as const)( - 'records the delta for a %s transaction through the shared Stripe recorder', - async (trigger) => { - const { create, settle } = setup() - await settle({ ...context, trigger }) - - expect(create).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ - amount: 1, - currency: 'usd', - confirm: true, - payment_method_data: { type: 'crypto' }, - payment_method_types: ['crypto'], - payment_method_options: { - crypto: { - mode: 'transaction_verification', - transaction_verification_options: { - network: 'tempo', - transaction_hash: context.txHash, - }, - }, - }, - application_fee_amount: 1, - metadata: expect.objectContaining({ - mpp_sdk: sdkIdentifier, - integration: 'sessions', - }), - }), - { - apiVersion: stripePreviewVersion, - idempotencyKey: context.txHash, - stripeAccount: 'acct_test', - }, - ) - }, - ) - - test('also records sessions added to defaultMethods()', async () => { - const { create, settle } = setup({ additional: true }) - await settle(context) - expect(create).toHaveBeenCalledTimes(1) - }) - - test.each([0n, 1n, 5_000n, 9_999n])( - 'skips a %s raw-unit delta but calls the merchant hook', - async (delta) => { - const onSessionSettlement = vi.fn() - const { create, settle } = setup({ onSessionSettlement }) - const event = { ...context, trigger: 'close' as const, delta } - await settle(event) - expect(create).not.toHaveBeenCalled() - expect(onSessionSettlement).toHaveBeenCalledExactlyOnceWith(event) - }, - ) - - test('floors each transaction independently without carrying fractional cents', async () => { - const { create, settle } = setup() - await settle({ ...context, delta: 19_999n }) - await settle({ ...context, txHash: `0x${'ef'.repeat(32)}`, amount: 69_998n, delta: 19_999n }) - expect(create.mock.calls.map(([params]) => (params as any).amount)).toEqual([1, 1]) - }) - - test('uses the same idempotency key when a settlement is delivered again', async () => { - const { create, settle } = setup() - await settle(context) - await settle(context) - expect(create.mock.calls[0]).toEqual(create.mock.calls[1]) - }) - - test('records before calling the merchant hook', async () => { - const onSessionSettlement = vi.fn(() => { - expect(create).toHaveBeenCalledTimes(1) - }) - const { create, settle } = setup({ onSessionSettlement }) - await settle(context) - expect(onSessionSettlement).toHaveBeenCalledExactlyOnceWith(context) - }) - - test('a merchant hook failure does not prevent recording', async () => { - const error = new Error('merchant hook failed') - const { create, settle } = setup({ - onSessionSettlement: () => { - throw error - }, - }) - await expect(settle(context)).rejects.toThrow(error) - expect(create).toHaveBeenCalledTimes(1) - }) - - test('logs Stripe failures without failing settlement or skipping the merchant hook', async () => { - const onSessionSettlement = vi.fn() - const { create, settle } = setup({ onSessionSettlement }) - const error = new Error('Stripe unavailable') - create.mockRejectedValueOnce(error) - const log = vi.spyOn(console, 'error').mockImplementation(() => {}) - await expect(settle(context)).resolves.toBeUndefined() - expect(log).toHaveBeenCalledWith('[stripe] failed to record crypto payment:', error) - expect(onSessionSettlement).toHaveBeenCalledExactlyOnceWith(context) - }) - - test('inherits the shared recorder fallback when Stripe rejects optional metadata', async () => { - const { create, settle } = setup() - create.mockRejectedValueOnce({ type: 'StripeInvalidRequestError' }) - vi.spyOn(console, 'warn').mockImplementation(() => {}) - await settle(context) - expect(create).toHaveBeenCalledTimes(2) - expect(create).toHaveBeenLastCalledWith( - expect.objectContaining({ - amount: 1, - metadata: { machine_payment: 'true', mpp_sdk: sdkIdentifier }, - }), - expect.objectContaining({ idempotencyKey: `${context.txHash}_fallback` }), - ) - }) -}) diff --git a/src/stripe/server/Methods.test.ts b/src/stripe/server/Methods.test.ts index 0fbf9b1c..db6ce7a7 100644 --- a/src/stripe/server/Methods.test.ts +++ b/src/stripe/server/Methods.test.ts @@ -5,6 +5,7 @@ import { describe, expect, test, vi } from 'vp/test' import { sdkIdentifier } from '../../internal/version.js' import * as Method from '../../Method.js' import type { AnyServer } from '../../Method.js' +import * as TempoSession from '../../tempo/session/server/Session.js' import * as z from '../../zod.js' import type { StripeClient } from '../internal/types.js' @@ -912,3 +913,75 @@ describe('stripe.create() graceful degradation', () => { expect(findMethod(withAdditional, 'stripe', 'charge')).toBeDefined() }) }) + +describe('Stripe session settlement recording', () => { + const recipient = '0x1111111111111111111111111111111111111111' as stripe.DepositAddress<'tempo'> + const event = { + txHash: `0x${'ab'.repeat(32)}` as const, + channelId: `0x${'cd'.repeat(32)}` as const, + trigger: 'scheduled' as const, + amount: 50_000n, + delta: 10_000n, + } + + test.each([false, true])('records automatically (additional: %s)', async (additional) => { + const client = createMockStripeClient() + const mp = stripe({ + client, + networkId: 'test-profile', + livemode: true, + depositAddresses: { tempo: recipient }, + }) + const session = vi.spyOn(TempoSession, 'session') + try { + if (additional) mp.defaultMethods().additional({ tempo: { session: {} } }) + else mp.tempo.session({ recipient }) + await session.mock.calls.at(-1)![0]!.onSessionSettlement!(event) + expect(client.paymentIntents.create).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + amount: 1, + payment_method_options: { + crypto: { + mode: 'transaction_verification', + transaction_verification_options: { + network: 'tempo', + transaction_hash: event.txHash, + }, + }, + }, + }), + expect.objectContaining({ idempotencyKey: event.txHash }), + ) + } finally { + session.mockRestore() + } + }) + + test.each([ + [0n, 0], + [9_999n, 0], + [19_999n, 1], + ] as const)( + 'records %s raw units as %s cents and preserves the merchant callback', + async (delta, cents) => { + const client = createMockStripeClient() + const mp = stripe({ client, networkId: 'test-profile', livemode: true }) + const onSessionSettlement = vi.fn() + const session = vi.spyOn(TempoSession, 'session') + try { + mp.tempo.session({ recipient, onSessionSettlement }) + const context = { ...event, trigger: 'close' as const, delta } + await session.mock.calls.at(-1)![0]!.onSessionSettlement!(context) + expect(client.paymentIntents.create).toHaveBeenCalledTimes(cents ? 1 : 0) + if (cents) + expect(client.paymentIntents.create).toHaveBeenCalledWith( + expect.objectContaining({ amount: cents }), + expect.anything(), + ) + expect(onSessionSettlement).toHaveBeenCalledExactlyOnceWith(context) + } finally { + session.mockRestore() + } + }, + ) +}) From 6d26a8e2dd4bf7ebbad92e962d471981cc5631e4 Mon Sep 17 00:00:00 2001 From: Karan Narula Date: Tue, 15 Sep 2026 11:40:04 -0400 Subject: [PATCH 3/6] fix(stripe): identify automatic session recordings in analytics Committed-By-Agent: codex Co-authored-by: codex --- .changeset/stripe-session-paymentintents.md | 2 +- src/stripe/server/Methods.test.ts | 1 + src/stripe/server/Methods.ts | 13 ++++++++++--- src/stripe/server/internal/analytics.ts | 5 +++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.changeset/stripe-session-paymentintents.md b/.changeset/stripe-session-paymentintents.md index fc239fe9..b440f36f 100644 --- a/.changeset/stripe-session-paymentintents.md +++ b/.changeset/stripe-session-paymentintents.md @@ -2,4 +2,4 @@ 'mppx': patch --- -Added automatic Stripe PaymentIntent recording for Tempo session settlements, using each transaction's newly settled amount rounded down to whole cents. Preserved optional settlement callbacks after recording; applications no longer need to create PaymentIntents in those callbacks. +Added automatic Stripe PaymentIntent recording for Tempo session settlements, using each transaction's newly settled amount rounded down to whole cents and recording the session intent in analytics metadata. Preserved optional settlement callbacks after recording; applications no longer need to create PaymentIntents in those callbacks. diff --git a/src/stripe/server/Methods.test.ts b/src/stripe/server/Methods.test.ts index db6ce7a7..29c62a4c 100644 --- a/src/stripe/server/Methods.test.ts +++ b/src/stripe/server/Methods.test.ts @@ -940,6 +940,7 @@ describe('Stripe session settlement recording', () => { expect(client.paymentIntents.create).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ amount: 1, + metadata: expect.objectContaining({ mpp_intent: 'session' }), payment_method_options: { crypto: { mode: 'transaction_verification', diff --git a/src/stripe/server/Methods.ts b/src/stripe/server/Methods.ts index 0df8c9b0..07c71202 100644 --- a/src/stripe/server/Methods.ts +++ b/src/stripe/server/Methods.ts @@ -273,6 +273,7 @@ export function stripe(parameters: P): Stripe const amount = (context.delta / 10_000n) * 10_000n if (amount > 0n) await tempoPaymentHandler({ + intent: 'session', receipt: { reference: context.txHash }, request: { amount: amount.toString() }, }) @@ -477,8 +478,14 @@ function createPaymentSuccessHandler( connect?: ConnectConfig, metadata?: Record, ) { - return (params: { challenge?: any; receipt: any; request: any; requestInput?: any }) => { - const { challenge, receipt, request, requestInput } = params + return (params: { + challenge?: any + intent?: string + receipt: any + request: any + requestInput?: any + }) => { + const { challenge, intent, receipt, request, requestInput } = params if (receipt?.reference && request?.amount) { const paymentIntentOptionsInput = requestInput?.paymentIntentOptions as | PaymentIntent.OptionsInput @@ -498,7 +505,7 @@ function createPaymentSuccessHandler( reference: receipt.reference, amount: String(request.amount), ...(connect && { connect }), - analyticsMetadata: buildAnalytics({ challenge }), + analyticsMetadata: buildAnalytics({ challenge, intent }), ...(Object.keys(resolvedPaymentIntentOptions).length > 0 && { paymentIntentOptions: resolvedPaymentIntentOptions, }), diff --git a/src/stripe/server/internal/analytics.ts b/src/stripe/server/internal/analytics.ts index c789e369..5fe8e887 100644 --- a/src/stripe/server/internal/analytics.ts +++ b/src/stripe/server/internal/analytics.ts @@ -5,14 +5,15 @@ import { machinePaymentMetadata } from '../../internal/constants.js' /** Builds Stripe metadata used to identify and analyze MPP payments. */ export function buildAnalytics(parameters: { challenge?: Pick | undefined + intent?: string | undefined }): Record { - const { challenge } = parameters + const { challenge, intent = challenge?.intent } = parameters const metadata = { ...machinePaymentMetadata, mpp_sdk: sdkIdentifier, + ...(intent && { mpp_intent: intent }), ...(challenge && { mpp_challenge_id: challenge.id, - mpp_intent: challenge.intent, }), } return Object.fromEntries( From 47b2590a645995d163e025d7c257e6935d997e4f Mon Sep 17 00:00:00 2001 From: Karan Narula Date: Thu, 17 Sep 2026 11:41:45 -0400 Subject: [PATCH 4/6] Reuse shared recorder amount flooring for sessions Committed-By-Agent: codex Co-authored-by: codex --- src/stripe/server/Methods.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/stripe/server/Methods.ts b/src/stripe/server/Methods.ts index 07c71202..691d1d74 100644 --- a/src/stripe/server/Methods.ts +++ b/src/stripe/server/Methods.ts @@ -268,15 +268,11 @@ export function stripe(parameters: P): Stripe ...(hostedTempoFeePayer && { feePayer: hostedTempoFeePayer }), ...rest, async onSessionSettlement(context) { - // Stripe verifies each transaction in whole cents. Never round a - // settlement up or carry its sub-cent remainder into another transaction. - const amount = (context.delta / 10_000n) * 10_000n - if (amount > 0n) - await tempoPaymentHandler({ - intent: 'session', - receipt: { reference: context.txHash }, - request: { amount: amount.toString() }, - }) + await tempoPaymentHandler({ + intent: 'session', + receipt: { reference: context.txHash }, + request: { amount: context.delta.toString() }, + }) await onSessionSettlement?.(context) }, } as tempoSession.Parameters) as Method.AnyServer From d3d87d2ae2ae51d8731c534e8cc658829b3acaff Mon Sep 17 00:00:00 2001 From: Karan Narula Date: Thu, 17 Sep 2026 11:50:04 -0400 Subject: [PATCH 5/6] Remove redundant session method documentation Committed-By-Agent: codex Co-authored-by: codex --- src/stripe/server/Methods.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/stripe/server/Methods.ts b/src/stripe/server/Methods.ts index 691d1d74..6d28a818 100644 --- a/src/stripe/server/Methods.ts +++ b/src/stripe/server/Methods.ts @@ -99,10 +99,6 @@ interface StripeMachinePayments

metadata?: Record } & Partial[0], 'currency' | 'recipient'>>, ) => TempoServer - /** - * Creates a session that automatically records whole-cent settlement deltas - * as Stripe PaymentIntents. `onSessionSettlement` runs after recording. - */ session: ( params: { recipient: stripe.DepositAddress<'tempo'> } & Omit< tempoSession.Parameters, From f619706f46c2ac0dcc62096c3543d6655aab5687 Mon Sep 17 00:00:00 2001 From: Karan Narula Date: Thu, 17 Sep 2026 11:55:51 -0400 Subject: [PATCH 6/6] Simplify Stripe session recording tests Committed-By-Agent: codex Co-authored-by: codex --- src/stripe/server/Methods.test.ts | 61 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/src/stripe/server/Methods.test.ts b/src/stripe/server/Methods.test.ts index 29c62a4c..c6ea1e35 100644 --- a/src/stripe/server/Methods.test.ts +++ b/src/stripe/server/Methods.test.ts @@ -924,7 +924,35 @@ describe('Stripe session settlement recording', () => { delta: 10_000n, } - test.each([false, true])('records automatically (additional: %s)', async (additional) => { + test('records automatically through tempo.session()', async () => { + const client = createMockStripeClient() + const mp = stripe({ client, networkId: 'test-profile', livemode: true }) + const session = vi.spyOn(TempoSession, 'session') + try { + mp.tempo.session({ recipient }) + await session.mock.calls.at(-1)![0]!.onSessionSettlement!(event) + expect(client.paymentIntents.create).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + amount: 1, + metadata: expect.objectContaining({ mpp_intent: 'session' }), + payment_method_options: { + crypto: { + mode: 'transaction_verification', + transaction_verification_options: { + network: 'tempo', + transaction_hash: event.txHash, + }, + }, + }, + }), + expect.objectContaining({ idempotencyKey: event.txHash }), + ) + } finally { + session.mockRestore() + } + }) + + test('records automatically through defaultMethods().additional()', async () => { const client = createMockStripeClient() const mp = stripe({ client, @@ -934,8 +962,7 @@ describe('Stripe session settlement recording', () => { }) const session = vi.spyOn(TempoSession, 'session') try { - if (additional) mp.defaultMethods().additional({ tempo: { session: {} } }) - else mp.tempo.session({ recipient }) + mp.defaultMethods().additional({ tempo: { session: {} } }) await session.mock.calls.at(-1)![0]!.onSessionSettlement!(event) expect(client.paymentIntents.create).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ @@ -957,32 +984,4 @@ describe('Stripe session settlement recording', () => { session.mockRestore() } }) - - test.each([ - [0n, 0], - [9_999n, 0], - [19_999n, 1], - ] as const)( - 'records %s raw units as %s cents and preserves the merchant callback', - async (delta, cents) => { - const client = createMockStripeClient() - const mp = stripe({ client, networkId: 'test-profile', livemode: true }) - const onSessionSettlement = vi.fn() - const session = vi.spyOn(TempoSession, 'session') - try { - mp.tempo.session({ recipient, onSessionSettlement }) - const context = { ...event, trigger: 'close' as const, delta } - await session.mock.calls.at(-1)![0]!.onSessionSettlement!(context) - expect(client.paymentIntents.create).toHaveBeenCalledTimes(cents ? 1 : 0) - if (cents) - expect(client.paymentIntents.create).toHaveBeenCalledWith( - expect.objectContaining({ amount: cents }), - expect.anything(), - ) - expect(onSessionSettlement).toHaveBeenCalledExactlyOnceWith(context) - } finally { - session.mockRestore() - } - }, - ) })