feat(payments): add recurring payment update API#547
Conversation
|
@greptileai review |
|
@greptileai review |
Greptile SummaryThis PR introduces
Confidence Score: 5/5Safe 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
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]
%%{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]
Reviews (11): Last reviewed commit: "PRO-1440 allow expired due retry recover..." | Re-trigger Greptile |
|
@greptileai review |
|
@greptileai review |
|
@greptileai review |
|
@greptileai review |
|
@greptileai review |
|
@greptileai review |
|
@greptileai review |
|
@greptileai review |
Summary
PATCH /v1/payments/recurring-payments/{id}with recurring-payment feature guard and write/read permission wiring.updatingstatus, durable update attempts, immutable update audit events, repository methods, dashboard API proxy, and generated Postman docs.Local validation
pnpm -C apps/sdp-api openapi:generatepnpm -C apps/sdp-docs generate:apipnpm --filter @sdp/api db:migrate:testpnpm --filter @sdp/api typecheckpnpm --filter @sdp/types typecheckpnpm --filter sdp-web typecheckpnpm --filter @sdp/api lint(passes; existing repo warnings remain)pnpm --filter @sdp/api test:node -- src/routes/payments/schemas.test.tspnpm -C apps/sdp-api exec vitest run --config vitest.workers.config.ts src/routes/wallet-scope-route-coverage.test.ts --reporter verbose --testTimeout=10000pnpm -C apps/sdp-api exec vitest run --config vitest.workers.config.ts src/openapi/spec.test.ts --reporter verbose --testTimeout=10000pnpm -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 testcurrently fails onlysrc/routes/rpc.test.tsprovider proxy checks: 4 tests expect 200 but receive 502 from RPC relay provider connectivity checks.pnpm --filter sdp-web lintfails before linting app code witheslint-plugin-react/ ESLint 10 runtime error:contextOrFilename.getFilename is not a functionwhile loadingreact/display-name.updatingstatus.Closes PRO-1440