Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/Receipt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,42 @@ describe('from', () => {
}),
).toThrow()
})

test('behavior: preserves method-specific extension fields', () => {
const receipt = Receipt.from({
method: 'nearintents',
reference: 'FtChYxxQh1k6vKjQ9wq5q1f8s2n3p4r5t6u7v8w9x0yz',
status: 'success',
timestamp: '2025-01-21T12:00:00.000Z',
challengeId: 'qB3wErTyU7iOpAsD9fGhJk',
originTxHash: '0x9bcff372aee89b648c922b850573b22387c31d693079f5e37cd255814e2d615a',
destinationNetwork: 'near:mainnet',
})

expect(receipt).toMatchInlineSnapshot(`
{
"challengeId": "qB3wErTyU7iOpAsD9fGhJk",
"destinationNetwork": "near:mainnet",
"method": "nearintents",
"originTxHash": "0x9bcff372aee89b648c922b850573b22387c31d693079f5e37cd255814e2d615a",
"reference": "FtChYxxQh1k6vKjQ9wq5q1f8s2n3p4r5t6u7v8w9x0yz",
"status": "success",
"timestamp": "2025-01-21T12:00:00.000Z",
}
`)
})

test('error: rejects invalid base field even with extension fields present', () => {
expect(() =>
Receipt.from({
method: 'nearintents',
reference: 'FtChYxxQh1k6vKjQ9wq5q1f8s2n3p4r5t6u7v8w9x0yz',
status: 'failed' as 'success',
timestamp: '2025-01-21T12:00:00.000Z',
originTxHash: '0x9bcff372aee89b648c922b850573b22387c31d693079f5e37cd255814e2d615a',
}),
).toThrow()
})
})

describe('serialize', () => {
Expand Down Expand Up @@ -67,6 +103,23 @@ describe('deserialize', () => {
})
})

describe('serialize + deserialize', () => {
test('behavior: round-trips method-specific extension fields', () => {
const receipt = Receipt.from({
method: 'nearintents',
reference: 'FtChYxxQh1k6vKjQ9wq5q1f8s2n3p4r5t6u7v8w9x0yz',
status: 'success',
timestamp: '2025-01-21T12:00:00.000Z',
externalId: 'order_12345',
challengeId: 'qB3wErTyU7iOpAsD9fGhJk',
originTxHash: '0x9bcff372aee89b648c922b850573b22387c31d693079f5e37cd255814e2d615a',
destinationNetwork: 'near:mainnet',
})

expect(Receipt.deserialize(Receipt.serialize(receipt))).toEqual(receipt)
})
})

describe('fromResponse', () => {
test('behavior: extracts receipt from Payment-Receipt header', () => {
const encoded =
Expand Down
39 changes: 26 additions & 13 deletions src/Receipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,7 @@ import { Base64 } from 'ox'
import * as Constants from './Constants.js'
import * as z from './zod.js'

/**
* Schema for a payment receipt.
*
* @example
* ```ts
* import { Receipt } from 'mppx'
*
* const receipt = Receipt.Schema.parse(data)
* ```
*/
export const Schema = z.object({
const shape = {
/** Payment method used (e.g., "tempo", "stripe"). */
method: z.string(),
/** Method-specific reference (e.g., transaction hash). */
Expand All @@ -26,11 +16,34 @@ export const Schema = z.object({
status: z.literal('success'),
/** RFC 3339 settlement timestamp. */
timestamp: z.datetime(),
})
}

/** Base-field schema used only to derive the {@link Receipt} type without an index signature. */
const BaseSchema = z.object(shape)

/**
* Schema for a payment receipt.
*
* Method specifications may define additional receipt fields beyond the
* base set (per the core spec's Payment-Receipt section); unknown fields
* are preserved through parse/serialize round-trips rather than stripped.
*
* @example
* ```ts
* import { Receipt } from 'mppx'
*
* const receipt = Receipt.Schema.parse(data)
* ```
*/
export const Schema = z.looseObject(shape)

/**
* Payment receipt returned after verification.
*
* Method-specific extension fields are preserved at runtime but not part of
* this base type; method packages can type them via intersection
* (e.g. `Receipt.Receipt & { originTxHash: string }`).
*
* @example
* ```ts
* import { Receipt } from 'mppx'
Expand All @@ -43,7 +56,7 @@ export const Schema = z.object({
* }
* ```
*/
export type Receipt = z.infer<typeof Schema>
export type Receipt = z.infer<typeof BaseSchema>

/**
* Deserializes a Payment-Receipt header value to a receipt.
Expand Down
Loading