Add the payment-document contract for quotes that need supporting evidence - #1045
ls-bolt[bot] wants to merge 1 commit into
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. 3 Skipped Deployments
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
✱ Stainless preview builds for gridThis PR will update the cli go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-typescript studio · code · diff
✅ grid-openapi studio · code · diff
✅ grid-kotlin studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-go studio · code · diff
✅ grid-python studio · code · diff
✅ grid-php studio · code · diff
✅ grid-cli studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
be39092 to
4e8133c
Compare
|
⚡ Revision log — updated in place, latest first. Revision 2
Earlier revisions (1)Revision 1
|
…dence
Some corridors will not clear a payment without evidence of what it is for. A
business payout to China is the case driving this: the documents it needs vary
by purposeOfPayment, and the payment cannot go out until they are all in.
A quote that needs documents now comes back with documentStatus: PENDING and a
documentRequirements array, and POST /quotes/{quoteId}/documents uploads one
file per requirement. The response is the updated quote, so the caller watches
satisfied flip per requirement and documentStatus reach COMPLETE on the last
one. A quote that needs no documents omits both fields.
QuoteRequest gains an optional serviceCategory, which a China B2B CNY
SERVICE_CHARGES payment requires at runtime. The rule is route-specific and
OpenAPI cannot express it cleanly, so the field stays optional in the schema
and the description carries the condition.
Contract only. The handler, persistence, and provider calls are webdev work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4e8133c to
4dd5518
Compare

Summary
Some corridors will not clear a payment without evidence of what it is for. A business payout to China is the case driving this: the documents it needs vary by
purposeOfPayment, and the payment cannot go out until they are all in. Today the API has no way to express that, so there is nothing a platform can build against.This adds the contract. A quote that needs documents comes back with
documentStatus: PENDINGand adocumentRequirementsarray, and a new endpoint uploads one file per requirement:POST /quotesreturns the quote with its requirements and nopaymentInstructions.POST /quotes/{quoteId}/documentsuploads one file, naming the requirement it satisfies and what the file is.satisfiedflip per requirement.documentStatustoCOMPLETE, after which the quote can be executed.A quote that needs no documents omits both fields, so nothing changes for existing integrations.
This is the contract only. The handler, persistence, and provider calls are webdev work — see the dependency note below.
Design notes
One file per requirement.
acceptedDocumentTypeslists alternatives for a single file, not a list of files to send. ASUPPORTING_PROOFrequirement acceptingPURCHASE_ORDERorDELIVERY_SLIPstill takes one upload;documentTypesays which one you sent.Requirement ID and document type are separate enums. The ID names the slot to fill and is a stable policy key; the type names what the file is. They coincide often enough to look redundant, but diverge exactly where a requirement accepts alternatives —
COMMERCIAL_AGREEMENTis satisfied by a purchase order, invoice, or contract. Both are closed enums so a generated client cannot send an arbitrary key.serviceCategoryis optional in the schema. It is required at runtime only for a China B2B CNY payment whosepurposeOfPaymentisSERVICE_CHARGES. OpenAPI cannot express a route-specific conditional cleanly, so the field stays optional and the description carries the condition, with the failure documented as400 INVALID_INPUTonPOST /quotes.Uploads are final. The first file accepted for a requirement satisfies it; there is no replace or delete. Correcting a wrong file means creating a new quote. This keeps the contract honest about what the provider has already been sent.
Document types are logical, not provider enums.
PaymentDocumentTypedescribes the evidence, not a Thunes attachment type. The mapping is unresolved for customs declarations, logistics bills, and bills of lading, and belongs to the webdev implementation.Changes: 17 files
New reusable schemas under
openapi/components/schemas/quotes/:QuoteDocumentStatus—PENDING/COMPLETEQuoteDocumentRequirement—requirementId, non-emptyacceptedDocumentTypes,satisfiedQuoteDocumentRequirementId— the 12 policy keysPaymentDocumentType— the 10 logical evidence typesChinaB2BServiceCategory— the 4 service categoriesQuoteDocumentUploadRequest/QuoteDocumentUploadRequestBody— the multipart bodyModified:
openapi/paths/quotes/quotes_{quoteId}_documents.yaml(new) —uploadQuoteDocument, taggedCross-Currency Transfers, BasicAuth, responses 200/400/401/404/409/410/500openapi/openapi.yaml— register the routeQuote.yaml— optionaldocumentStatusanddocumentRequirements;paymentInstructionsnotes it is absent whilePENDINGQuoteRequest.yaml— optionalserviceCategory;immediatelyExecutedocuments the unsupported combinationquotes.yaml— the400names both new causes, the201describes thePENDINGshape, plus a China B2B request examplequotes_{quoteId}_execute.yaml— the existing409now namesDOCUMENTS_REQUIREDError409.yaml—DOCUMENTS_REQUIRED,DOCUMENT_REQUIREMENT_ALREADY_SATISFIED,DOCUMENT_UPLOAD_IN_PROGRESS,QUOTE_NOT_ACCEPTING_DOCUMENTSError410.yaml—QUOTE_EXPIRED.stainless/stainless.yml— map the new endpoint to aquotes.upload_documentSDK method, and registerQuoteDocumentRequirementas a named modelopenapi.yaml,mintlify/openapi.yaml— regeneratedAdditive throughout, so
info.versionis unchanged.Test plan
Node 22. Commands and results:
make build— bundles regenerate; the two bundles are byte-identical.make lint-openapi— exit 0. Redocly: "Your API description is valid", 2 warnings. Diffing every Redocly and Spectral finding againstorigin/maingives exactly one new line: aninformation-level "missing example" onQuoteDocumentUploadRequest.properties.file. The existingBaseDocumentRequest.properties.filecarries the identical finding — a binary field has no sensible example, so this matches the house convention./quotes/{quoteId}/documentsand all seven new schemas, each as a stable top-level component name (no inline anonymous objects, no_1suffixes) — so the webdev Python client gets predictable names.PENDINGwith 3 requirements and no instructions → three uploads →COMPLETEwith instructions available. Also checked that a quote omitting both fields is valid, and that the schema rejects an unknownrequirementId, an unknown document type, and an emptyacceptedDocumentTypes.git diff --checkpasses. The only deleted lines in the generated bundle are the three descriptions this PR intentionally rewrites; everything else is pure insertion.Adversarial review flagged that the multipart
filepart had noencoding.contentType, so it would default toapplication/octet-stream— which this endpoint rejects. Fixed: the encoding now declaresapplication/pdf, image/jpeg, image/png.Dependencies and follow-ups
Depends on the China B2B purposes of payment (#1041), already merged.
The runtime behind this contract is webdev #35818 (purpose values and CNY guard), #35903 (requirement discovery), and #35822 (single attachment upload). Those are unchanged by this PR. The enum values here were cross-checked against #35903's policy module so the contract matches what the runtime will actually return.
On
.stainless/stainless.ymlI initially left this out as out-of-scope, reasoning that the file is maintained out-of-band here —
POST /cards/{id}/tokenizeand the sandbox card-dispute simulators are absent from it too. CI disagreed, and it was right: the Stainless preview reportedEndpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: post /quotes/{quoteId}/documentson all 8 SDK targets, each flagged as "a regression from the base state". The precedent I leaned on didn't actually cover this case — #985's endpoints produced no skip diagnostic.So the endpoint is now mapped as
quotes.upload_document. The name follows thedocumentsresource, which is the closest analogue (also multipart, also an upload) and uses the sameendpoint+body_param_namepair. That key becomes the public method name in all eight SDKs, so rename it here if you'd prefer something else — it's cheaper to change now than after an SDK release.Fixing that surfaced a second, different diagnostic:
Model/RecommendedforQuoteDocumentRequirement. It is registered asquote_document_requirement, so the requirement object gets a named type in each SDK instead of an anonymous inline one — which is what the "stable names for the webdev Python client" requirement needs. The new enums are deliberately not registered: this repo registers almost no enums as models (TransactionStatusis the lone exception), and Stainless did not ask for them.Public
Quotes that require supporting documents now report what is needed and accept uploads at
POST /quotes/{quoteId}/documents.