Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,9 @@ XVCJ
Yapily
Zalopay
Zalora
codegen
datamodel
pisp
PISP
pyca
SECP
8 changes: 8 additions & 0 deletions code/sdk/python/ap2/sdk/generated/payment_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class PaymentReceiptSuccess(BaseModel):
...,
description='A unique identifier for the transaction confirmation at the network. Present only if status is Success.',
)
rail_confirmation_verified: bool | None = Field(
default=None,
description='Meaningful only if status is Success. True only if the issuer independently confirmed psp_confirmation_id and network_confirmation_id against the actual payment rail (PSP/network) before issuing this receipt, rather than merely generating them. Absent or false means these confirmation IDs are self-declared by the issuer and MUST NOT be treated as evidence that payment-rail settlement occurred: presence of a confirmation ID proves the issuer asserted a claim, not that the claim was checked.',
)


class PaymentReceiptError(BaseModel):
Expand Down Expand Up @@ -82,6 +86,10 @@ class PaymentReceiptError(BaseModel):
default=None,
description='A unique identifier for the transaction confirmation at the network. Present only if status is Success.',
)
rail_confirmation_verified: bool | None = Field(
default=None,
description='Meaningful only if status is Success. True only if the issuer independently confirmed psp_confirmation_id and network_confirmation_id against the actual payment rail (PSP/network) before issuing this receipt, rather than merely generating them. Absent or false means these confirmation IDs are self-declared by the issuer and MUST NOT be treated as evidence that payment-rail settlement occurred: presence of a confirmation ID proves the issuer asserted a claim, not that the claim was checked.',
)


class PaymentReceipt(RootModel[PaymentReceiptSuccess | PaymentReceiptError]):
Expand Down
23 changes: 21 additions & 2 deletions code/sdk/python/ap2/sdk/receipt_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,27 @@ def create_payment_receipt(
self,
payment_mandate_content: PaymentMandate,
reference: str,
psp_confirmation_id: str | None = None,
network_confirmation_id: str | None = None,
) -> PaymentReceipt:
"""Creates a PaymentReceipt model instance.

Args:
payment_mandate_content: The closed payment mandate whose PISP this
receipt inherits as its issuer (when present).
reference: The payment mandate reference this receipt binds to.
psp_confirmation_id: The PSP's own confirmation identifier for this
payment, obtained by actually checking the payment rail (e.g. the
PSP's settlement/charge API response). If omitted, a locally
generated placeholder is used instead and the receipt's
`rail_confirmation_verified` field is left unset -- callers MUST
NOT treat the resulting receipt as proof that payment-rail
settlement occurred unless they supply a real, rail-checked value
here (see https://github.com/google-agentic-commerce/AP2/issues/327).
network_confirmation_id: The network's own confirmation identifier
for this payment. Same caveat as `psp_confirmation_id`: omitting
it means the receipt's confirmation IDs are self-declared, not
independently verified.

Returns:
A PaymentReceipt model instance.
Expand All @@ -67,11 +81,16 @@ def create_payment_receipt(
issuer=issuer,
reference=reference,
)
rail_confirmation_verified = (
psp_confirmation_id is not None
and network_confirmation_id is not None
)
return PaymentReceipt(
**base,
payment_id=payment_id,
psp_confirmation_id=payment_id,
network_confirmation_id=payment_id,
psp_confirmation_id=psp_confirmation_id or payment_id,
network_confirmation_id=network_confirmation_id or payment_id,
rail_confirmation_verified=rail_confirmation_verified,
)

def create_checkout_receipt(
Expand Down
40 changes: 40 additions & 0 deletions code/sdk/python/ap2/tests/receipt_wrapper_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,46 @@ def test_create_payment_receipt_no_pisp(issuer_key):
assert receipt.root.iss == ''


def test_create_payment_receipt_defaults_to_unverified(issuer_key):
"""Without real rail confirmation IDs, the receipt is honestly unverified.

Regression test for
https://github.com/google-agentic-commerce/AP2/issues/327: a Success
receipt must not silently imply payment-rail evidence it does not have.
"""
client = ReceiptClient()
reference = 'test_reference'
payment_mandate_content = _payment_mandate(pisp=None)

receipt = client.create_payment_receipt(payment_mandate_content, reference)

assert receipt.root.status == 'Success'
# Placeholder confirmation IDs are still populated (unchanged behavior for
# existing callers) ...
assert receipt.root.psp_confirmation_id == receipt.root.payment_id
assert receipt.root.network_confirmation_id == receipt.root.payment_id
# ... but the receipt now says so honestly instead of staying silent.
assert receipt.root.rail_confirmation_verified is False


def test_create_payment_receipt_with_real_rail_confirmation(issuer_key):
"""Real, caller-supplied confirmation IDs mark the receipt as verified."""
client = ReceiptClient()
reference = 'test_reference'
payment_mandate_content = _payment_mandate(pisp=None)

receipt = client.create_payment_receipt(
payment_mandate_content,
reference,
psp_confirmation_id='psp-real-123',
network_confirmation_id='network-real-456',
)

assert receipt.root.psp_confirmation_id == 'psp-real-123'
assert receipt.root.network_confirmation_id == 'network-real-456'
assert receipt.root.rail_confirmation_verified is True


def test_create_checkout_receipt():
"""Test creation of a CheckoutReceipt."""
client = ReceiptClient()
Expand Down
4 changes: 4 additions & 0 deletions code/sdk/schemas/ap2/payment_receipt.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"network_confirmation_id": {
"type": "string",
"description": "A unique identifier for the transaction confirmation at the network. Present only if status is Success."
},
"rail_confirmation_verified": {
"type": "boolean",
"description": "Meaningful only if status is Success. True only if the issuer independently confirmed psp_confirmation_id and network_confirmation_id against the actual payment rail (PSP/network) before issuing this receipt, rather than merely generating them. Absent or false means these confirmation IDs are self-declared by the issuer and MUST NOT be treated as evidence that payment-rail settlement occurred: presence of a confirmation ID proves the issuer asserted a claim, not that the claim was checked."
}
},
"required": [
Expand Down
Loading