Skip to content

feat(api): add customer blocklist and manual payment retry - #279

Merged
aagarwal1012 merged 1 commit into
mainfrom
stlc-generated-33521978397
Sep 1, 2026
Merged

feat(api): add customer blocklist and manual payment retry#279
aagarwal1012 merged 1 commit into
mainfrom
stlc-generated-33521978397

Conversation

@aagarwal1012

Copy link
Copy Markdown
Member

Spec 1.113.6 -> 1.113.13 (+5 paths, +12 schemas, none removed).

Adds a new blocklist resource exposing blocklist.customers with
list/create/retrieve/delete and a nested notes subresource (create/update),
plus two manual retry methods on payments (retry, retrieve_retry_state).

Generated by stlc run 33521978397 from dodopayments-sdk-config@ae64a0b.

Spec 1.113.6 -> 1.113.13 (+5 paths, +12 schemas, none removed).

Adds a new blocklist resource exposing blocklist.customers with
list/create/retrieve/delete and a nested notes subresource (create/update),
plus two manual retry methods on payments (retry, retrieve_retry_state).

The blocklist list endpoint takes page_number/page_size and returns an items
array, matching default_page_number_pagination, so it auto-detects like the
other page-number lists and needs no explicit paginated flag.

SubscriptionWebhookPayload is reachable through OutgoingWebhookData and the
subscription.* webhooks, so it needs no explicit model mapping.

Validated: 148 configured endpoints all resolve against the spec, 113 config
schema refs resolve, and all 295 distinct spec $refs resolve with none
dangling. The 18 uncovered spec paths are the pre-existing intentional
exclusions (Swagger UI /docs routes, internal connectors and discount-customer
endpoints); no newly added path is uncovered.

Stainless-Generated-From: 3588152
@aagarwal1012
aagarwal1012 merged commit 7558374 into main Sep 1, 2026
8 checks passed
@aagarwal1012
aagarwal1012 deleted the stlc-generated-33521978397 branch September 1, 2026 15:08

@dodo-squirrels dodo-squirrels Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: generated code is correct; two API-design items worth raising upstream

I verified this branch locally against the mock server built from the embedded spec (1.113.13):

  • ruff check clean; ruff format clean except a pre-existing README nit that also fails on main.
  • pyright reports 5 errors, all pre-existing and unrelated (optional httpx_aiohttp import, two pydantic override warnings in _models.py, one deprecation in tests/utils.py). Zero in the new code.
  • Full suite: 3548 passed, 11 skipped. The 260 blocklist/payments tests pass.
  • .stats.yml 137 -> 145 matches exactly the 8 new methods across the 5 new paths.
  • Smoke-tested both create overloads, retrieve/list/delete, both notes methods, and both payment retry methods end to end. Request bodies, query serialization (created_at_gte as ISO 8601), path templating/percent-encoding, and the empty-path-param and required_args guards all behave correctly.
  • Verified subscription.past_due unwraps through the discriminated union, and that the new Data payload classes are still Subscription subclasses at runtime, so existing isinstance checks and code typed against Subscription keep working.

The generated code faithfully reflects the spec. The items below are about the spec/config rather than this diff, so I'm not blocking.

1. Default retry policy conflicts with the documented terminal errors on POST /payments/{payment_id}/retry

The spec documents 409 ("A payment is already in progress, or the merchant must act elsewhere first") and 429 ("The per-invoice retry guardrail refused this send") for this endpoint. Both are terminal business errors, but the shared client retry policy in _base_client.py retries 408/409/429/5xx for every method, so client.payments.retry(id) will silently re-POST a money-moving request up to max_retries (default 2) more times with backoff before surfacing the error. The 409 case is the concerning one: re-POSTing precisely when the server says a payment is already in progress is the scenario where a duplicate charge attempt or an extra consumed sends_used could occur.

This client also sets no idempotency header, so the generated internal idempotency key is never transmitted and there is no HTTP-level dedupe protection on the automatic retries.

Cheapest fix, no SDK change required: have the API return x-should-retry: false on those 409/429 responses. _should_retry honors that header ahead of the status-code rules. Alternatively disable retries for this operation in the Stainless config. Confirming the endpoint dedupes server-side would also address it. The same 409-retry behavior applies to POST /blocklist/customers ("The customer is already blocked"), though there it is only wasteful rather than risky.

2. past_due_ends_at is reachable only from webhook payloads

SubscriptionStatus now includes past_due, and the grace-period deadline is added to the webhook data payload. But Subscription itself has no past_due_ends_at, and GET /subscriptions/{id} does not return it. A consumer that polls rather than consuming webhooks can observe status == "past_due" with no way to learn when the grace period ends. Worth adding the field to the subscription response schema.

3. Minor / non-blocking

  • The inlined webhook payload schema now produces 24 structurally identical Data(Subscription) classes (12 event types x 2 variants). Naming that schema in the spec would let the generator emit one shared model instead, and would let consumers annotate against a single type.
  • data narrows from Subscription to a per-event Data on 20 existing webhook event models. Runtime-compatible as noted above; the only visible effect is that code which constructs these models with a plain Subscription will now fail a type check. Almost certainly fine for response models, flagging for completeness.
  • Redundant duplicate import of BlockedCustomerSource in resources/blocklist/customers/customers.py (lines 31 and 33). Harmless, resolves to the same object, and matches an existing generator artifact present in 8 other resource files on main.
  • The PR description covers only the blocklist and retry additions, but the diff also carries the past_due subscription status and webhook event, the cancelled_by_merchant_grace_period_expired cancel reason, BalanceLedgerEntry.payout_id, and Customer.blocked_at / blocklist_entry_id. All additive, nothing removed, but worth listing so reviewers of the changelog see them.
  • The "repeat the create call until subscriptions_swept is true" contract is documented only on the response field docstrings, so it does not surface in the create method docstring. These operations have no summary/description in the spec, which is consistent with the rest of this API, but an operation description here would be more valuable than usual given that retry loop.

Nothing security-sensitive in the diff; the new fields are ordinary business data and the embedded spec references only the public test/live hosts.

cast_to=ManualRetryState,
)

def retry(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec documents `409` ("A payment is already in progress, or the merchant must act elsewhere first") and `429` ("The per-invoice retry guardrail refused this send") for this operation. Both are terminal, but the shared retry policy in `_base_client.py` retries 408/409/429/5xx for all methods, so this call will silently re-POST a money-moving request up to `max_retries` (default 2) more times before raising. No idempotency header is sent by this client either, so the automatic retries have no HTTP-level dedupe.

Suggest returning `x-should-retry: false` on those responses (honored ahead of the status-code rules) or disabling retries for this operation in the generator config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant