Skip to content

feat(payments): add recurring payment update API#547

Open
GuiBibeau wants to merge 9 commits into
mainfrom
codex/pro-1440-recurring-payments-update-api
Open

feat(payments): add recurring payment update API#547
GuiBibeau wants to merge 9 commits into
mainfrom
codex/pro-1440-recurring-payments-update-api

Conversation

@GuiBibeau

Copy link
Copy Markdown
Collaborator

Summary

  • Add PATCH /v1/payments/recurring-payments/{id} with recurring-payment feature guard and write/read permission wiring.
  • Support pending edits as direct DB updates, active metadata/schedule edits in place, and active term/source/destination/token replacements through durable on-chain plan/subscription replacement.
  • Add public updating status, durable update attempts, immutable update audit events, repository methods, dashboard API proxy, and generated Postman docs.

Local validation

  • pnpm -C apps/sdp-api openapi:generate
  • pnpm -C apps/sdp-docs generate:api
  • pnpm --filter @sdp/api db:migrate:test
  • pnpm --filter @sdp/api typecheck
  • pnpm --filter @sdp/types typecheck
  • pnpm --filter sdp-web typecheck
  • pnpm --filter @sdp/api lint (passes; existing repo warnings remain)
  • pnpm --filter @sdp/api test:node -- src/routes/payments/schemas.test.ts
  • pnpm -C apps/sdp-api exec vitest run --config vitest.workers.config.ts src/routes/wallet-scope-route-coverage.test.ts --reporter verbose --testTimeout=10000
  • pnpm -C apps/sdp-api exec vitest run --config vitest.workers.config.ts src/openapi/spec.test.ts --reporter verbose --testTimeout=10000
  • pnpm -C apps/sdp-api exec vitest run --config vitest.workers.config.ts src/routes/payments.test.ts --reporter verbose --testTimeout=30000 (93 passed)

Local blockers / notes

  • pnpm --filter @sdp/api test currently fails only src/routes/rpc.test.ts provider proxy checks: 4 tests expect 200 but receive 502 from RPC relay provider connectivity checks.
  • pnpm --filter sdp-web lint fails before linting app code with eslint-plugin-react / ESLint 10 runtime error: contextOrFilename.getFilename is not a function while loading react/display-name.
  • No new edit UI is included in this ticket; dashboard change is only the API proxy plus status-label coverage for the new updating status.

Closes PRO-1440

@linear

linear Bot commented Jun 29, 2026

Copy link
Copy Markdown

PRO-1440

@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sdp-docs Ready Ready Preview, Comment Jun 30, 2026 1:59am
sdp-web Ready Ready Preview, Comment Jun 30, 2026 1:59am

Request Review

@GuiBibeau

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@GuiBibeau GuiBibeau changed the title PRO-1440 Recurring payments update API feat(payments): add recurring payment update API Jun 29, 2026
@GuiBibeau

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces PATCH /v1/payments/recurring-payments/{id} — a durable, idempotent update endpoint that handles pending-activation payments as direct DB patches and active payments via an atomic three-step on-chain replacement (new plan creation, new subscription authorization, old subscription cancellation). Multiple follow-up commits addressed P1 findings from the first review round, including optimistic-lock guards for concurrent pending-activation writes, clampToMinimum to prevent post-commitment nextCollectionDueAt rejections from stranding payments, and try/catch coverage for the getLatestUpdateAttempt call inside the error handler.

  • DB migration (0022) adds the updating status to the recurring-payment constraint and two new tables with proper FK constraints, JSONB type checks, and covering indexes.
  • Service layer orchestrates three distinct update modes — pending direct-write, active metadata/schedule, and active replacement — each with staged attempt tracking and stale-recovery logic.
  • Dashboard proxy adds a PATCH handler that forwards the raw body and threads through tracing headers.

Confidence Score: 5/5

Safe to merge — all three update modes have the durable recovery logic and concurrent-write guards needed for production use.

The follow-up commits addressed the hard concurrency and deadlock scenarios from the first review: optimistic updated_at locking for pending-activation writes, clampToMinimum on both nextCollectionDueAt validations, and a wrapping try/catch around the error-handler DB query. Remaining observations are narrow edge cases that do not affect the active-payment replacement path.

apps/sdp-api/src/services/payments/recurring-payments.ts — updatePendingRecurringPayment does not recalculate next_collection_due_at when firstCollectionAt or periodHours changes.

Important Files Changed

Filename Overview
apps/sdp-api/src/services/payments/recurring-payments.ts Core of the PR — adds ~1500 lines for the three update modes. Previously flagged P1s around concurrent writes, post-commitment validation stranding, recovery deadlocks, and error-handler exposure all appear addressed. Minor: pending-activation updates do not recalculate next_collection_due_at when firstCollectionAt or periodHours changes.
apps/sdp-api/src/db/migrations/postgres/0022_payment_recurring_payment_updates.sql Adds updating to the status constraint, creates update_attempts and update_events tables with FK constraints, JSONB type checks, and covering indexes. Clean and non-destructive.
apps/sdp-api/src/db/repositories/payment-recurring-payments.repository.postgres.ts Adds updateRecurringPayment, claimRecurringPaymentUpdate, and the full update-attempt/event CRUD. CASE WHEN pattern for optional fields is consistent with existing methods.
apps/sdp-api/src/routes/payments/handlers/recurring-payments.ts New updateRecurringPayment handler follows the established pattern: param/body validation, scoped payment fetch, wallet resolution, and assertApiKeyWalletAccess for both current and next source wallets.
apps/sdp-api/src/routes/payments/schemas.ts updateRecurringPaymentSchema adds the PATCH body shape with correct Zod refinements: at-least-one-field guard and counterpartyId → counterpartyAccountId co-requirement.
apps/sdp-api/src/db/repositories/payment-recurring-payments.repository.ts New repository interface types and input shapes for update attempts, events, and the updateRecurringPayment / claimRecurringPaymentUpdate operations.
packages/sdp-types/src/payments.ts Adds updating to PaymentRecurringPaymentStatus and defines UpdatePaymentRecurringPaymentRequest with all nullable/optional fields matching the route schema.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[PATCH /v1/payments/recurring-payments/:id] --> B{Validate params + body}
    B -->|Invalid| ERR1[400 Bad Request]
    B -->|Valid| C[Fetch recurring payment]
    C -->|Not found| ERR2[404 Not Found]
    C -->|Found| D{Check status}
    D -->|pending_activation| E[resolveRecurringPaymentUpdate]
    E --> F[updatePendingRecurringPayment - Direct DB update with optimistic lock]
    F --> RESP[200 Updated payment]
    D -->|active / stale updating| G[recoverOrBlockLifecycleCollection]
    G --> H[resolveRecurringPaymentUpdate - compute diff]
    H -->|No changes| RESP
    H -->|Changes| I{requestedActiveUpdateMode}
    I --> J[claimRecurringPaymentUpdate - status to updating]
    J --> K[getOrCreateUpdateAttempt]
    K --> L{Mode?}
    L -->|metadata_schedule| M[runMetadataScheduleUpdate]
    M --> M1[activeNextCollectionDueAt - clampToMinimum on retry]
    M1 --> M4[finalizeMetadataScheduleUpdate - DB transaction]
    M4 --> RESP
    L -->|replacement| N[runReplacementUpdate]
    N --> N2[Create plan on-chain]
    N2 --> N4[Authorize new subscription]
    N4 --> N5[Cancel old subscription]
    N5 --> N7[finalizeReplacementUpdate - DB transaction]
    N7 --> RESP
    M4 -->|Error| ERR3[recordRecurringPaymentUpdateFailure]
    N7 -->|Error| ERR3
    ERR3 --> RETHROW[Re-throw original error]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[PATCH /v1/payments/recurring-payments/:id] --> B{Validate params + body}
    B -->|Invalid| ERR1[400 Bad Request]
    B -->|Valid| C[Fetch recurring payment]
    C -->|Not found| ERR2[404 Not Found]
    C -->|Found| D{Check status}
    D -->|pending_activation| E[resolveRecurringPaymentUpdate]
    E --> F[updatePendingRecurringPayment - Direct DB update with optimistic lock]
    F --> RESP[200 Updated payment]
    D -->|active / stale updating| G[recoverOrBlockLifecycleCollection]
    G --> H[resolveRecurringPaymentUpdate - compute diff]
    H -->|No changes| RESP
    H -->|Changes| I{requestedActiveUpdateMode}
    I --> J[claimRecurringPaymentUpdate - status to updating]
    J --> K[getOrCreateUpdateAttempt]
    K --> L{Mode?}
    L -->|metadata_schedule| M[runMetadataScheduleUpdate]
    M --> M1[activeNextCollectionDueAt - clampToMinimum on retry]
    M1 --> M4[finalizeMetadataScheduleUpdate - DB transaction]
    M4 --> RESP
    L -->|replacement| N[runReplacementUpdate]
    N --> N2[Create plan on-chain]
    N2 --> N4[Authorize new subscription]
    N4 --> N5[Cancel old subscription]
    N5 --> N7[finalizeReplacementUpdate - DB transaction]
    N7 --> RESP
    M4 -->|Error| ERR3[recordRecurringPaymentUpdateFailure]
    N7 -->|Error| ERR3
    ERR3 --> RETHROW[Re-throw original error]
Loading

Reviews (11): Last reviewed commit: "PRO-1440 allow expired due retry recover..." | Re-trigger Greptile

Comment thread apps/sdp-api/src/services/payments/recurring-payments.ts
@GuiBibeau

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Comment thread apps/sdp-api/src/services/payments/recurring-payments.ts
Comment thread apps/sdp-api/src/services/payments/recurring-payments.ts
@GuiBibeau

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Comment thread apps/sdp-api/src/services/payments/recurring-payments.ts Outdated
@GuiBibeau

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Comment thread apps/sdp-api/src/services/payments/recurring-payments.ts
@GuiBibeau

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@GuiBibeau

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@GuiBibeau

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Comment thread apps/sdp-api/src/services/payments/recurring-payments.ts
@GuiBibeau

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@GuiBibeau

Copy link
Copy Markdown
Collaborator Author

@greptileai review

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.

2 participants