feat: add task donations and connect payouts#97
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 21 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthroughAdds Stripe-backed task funding and executor payouts: new schema and server flows, Stripe Connect onboarding/status APIs, checkout and webhook handling, payout queueing/retries, task/fund UI updates, an admin payout page, and local-development safety/docs updates. ChangesTask Funding & Payout System
Sequence Diagram(s)sequenceDiagram
participant Donor
participant CheckoutRoute as "api/tasks/[id]/fund/checkout"
participant PaymentsServer as "task-funding/payments.server"
participant Stripe
participant Webhook as "api/stripe/webhook"
participant StatusServer as "task-funding/status.server"
Donor->>CheckoutRoute: POST amountCents and donor fields
CheckoutRoute->>PaymentsServer: createTaskFundingCheckoutSession(...)
PaymentsServer->>Stripe: create Checkout Session
Stripe-->>PaymentsServer: session id and URL
PaymentsServer-->>CheckoutRoute: checkout response
CheckoutRoute-->>Donor: redirect URL
Stripe->>Webhook: checkout.session.completed
Webhook->>PaymentsServer: recordTaskFundingCheckoutPaid(session)
PaymentsServer->>StatusServer: refresh funding totals
Donor->>StatusServer: read paidUsdCents and pledgedUsdCents
sequenceDiagram
participant TasksServer as "tasks.server"
participant PayoutsServer as "task-payouts.server"
participant Stripe
participant CronRoute as "api/cron/task-payouts"
TasksServer->>PayoutsServer: queueTaskPayoutForVerifiedClaim(...)
PayoutsServer->>PayoutsServer: compute readiness and available funding
alt Ready
PayoutsServer->>Stripe: create transfer
Stripe-->>PayoutsServer: transfer id
PayoutsServer->>PayoutsServer: mark payout transferred
else Not ready
PayoutsServer->>PayoutsServer: mark pending connect or pending funds
end
CronRoute->>PayoutsServer: retryDueTaskPayouts()
PayoutsServer->>PayoutsServer: reconcile queued payouts
PayoutsServer-->>CronRoute: retry summary
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR introduces an end-to-end “fund a task → verify work → automatically pay the worker” flow in the web app, backed by new DB ledger models, Stripe Checkout for donations, Stripe Connect onboarding for payees, and a cron-driven payout retry loop. It also replaces /fund with an EV-ranked task funding marketplace and adds local-dev safety guards around DB URLs and Stripe keys.
Changes:
- Add task funding payments (Checkout → webhook →
CommerceOrder+TaskFundingPayment) and include paid amounts in funding status calculations/UI. - Add Stripe Connect onboarding/status APIs + UI, plus
TaskPayoutqueuing/execution/retry via cron and an admin payout backlog page. - Add local dev guard to block non-local
DATABASE_URLand live Stripe keys by default; update/fundnavigation + page to show EV-ranked fundable tasks.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/web/vercel.json | Adds 15-minute cron trigger for task payout retries. |
| packages/web/src/lib/tasks.server.ts | Blocks claiming paid tasks until Stripe payouts are ready; queues payouts after verification. |
| packages/web/src/lib/task-payouts.server.ts | New payout ledger logic: readiness checks, Stripe transfer execution, retry loop. |
| packages/web/src/lib/task-funding/status.server.ts | Expands funding status to include paid payments + supporter aggregation. |
| packages/web/src/lib/task-funding/payments.server.ts | New task funding Checkout creation + webhook recording + target status refresh. |
| packages/web/src/lib/task-funding/tests/status.server.test.ts | Extends funding status tests to cover paid vs failed/refunded payments. |
| packages/web/src/lib/stripe-connect.server.ts | New Stripe Connect (v2) account creation, sync, onboarding link, and readiness helpers. |
| packages/web/src/lib/routes.ts | Updates /fund nav labeling/CTA to “Fund Tasks”. |
| packages/web/src/lib/tests/task-payouts.server.test.ts | Adds unit tests for payout eligibility and available-funds calculation. |
| packages/web/src/components/tasks/StripeConnectStatusPanel.tsx | New client panel to show payout readiness and start onboarding. |
| packages/web/src/components/task-funding/TaskFundingProgress.tsx | Updates wording (“supporters”, “funded or pledged”) and empty state copy. |
| packages/web/src/components/task-funding/TaskFundingCheckoutForm.tsx | New client checkout form for task funding. |
| packages/web/src/components/task-funding/tests/TaskFundingProgress.test.tsx | Updates assertions for new wording. |
| packages/web/src/app/tasks/[id]/page.tsx | Adds task funding section (progress + checkout) and payout onboarding panel for paid tasks. |
| packages/web/src/app/fund/page.tsx | Replaces /fund with ranked fundable tasks list + progress bars. |
| packages/web/src/app/fund/page.logged-out.md | Updates logged-out copy snapshot for the new /fund page. |
| packages/web/src/app/api/tasks/[id]/pledge/route.ts | Adds serialization for paidUsdCents / pledgedUsdCents. |
| packages/web/src/app/api/tasks/[id]/funding-status/route.ts | Adds serialization for paidUsdCents / pledgedUsdCents. |
| packages/web/src/app/api/tasks/[id]/fund/checkout/route.ts | New API to create a task funding Checkout session (validated via zod). |
| packages/web/src/app/api/stripe/webhook/route.ts | Routes task-funding Stripe events to new recorders; handles refunds/disputes. |
| packages/web/src/app/api/stripe/webhook/route.test.ts | Adds coverage for task-funding webhook behavior (no generic donation activity). |
| packages/web/src/app/api/stripe/connect/status/route.ts | New authed API to fetch (optionally sync) Stripe Connect status. |
| packages/web/src/app/api/stripe/connect/onboarding-link/route.ts | New authed API to create Stripe onboarding links. |
| packages/web/src/app/api/stripe/connect/account/route.ts | New authed API to create a connected account and return synced status. |
| packages/web/src/app/api/cron/task-payouts/route.ts | New cron endpoint to retry due payouts. |
| packages/web/src/app/admin/task-payouts/page.tsx | New admin page showing stuck/pending payouts and retry state. |
| packages/web/scripts/guard-local-dev-db.mjs | New local-dev guard to block remote DB URLs + live Stripe keys by default. |
| packages/web/package.json | Wires local-dev guard into dev / dev:fast. |
| packages/db/prisma/schema.prisma | Adds TaskFundingPayment, StripeConnectedAccount, TaskPayout models + enums/relations. |
| packages/db/prisma/migrations/20260701120000_add_task_funding_payments_and_payouts/migration.sql | Creates new tables/enums/indexes/foreign keys for funding + payouts. |
| docs/LOCAL_DB.md | Documents the new local-dev DB/Stripe safety overrides. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| enum TaskFundingPaymentStatus { | ||
| PENDING | ||
| PAID | ||
| FAILED | ||
| CANCELED | ||
| REFUNDED | ||
| DISPUTED | ||
| } |
PR review packetStart here
Review checklist
Changed files considered
Updated automatically when this PR's preview or visual review reruns. |
Sentry preview audit failedThe preview smoke test ran, but the Sentry audit could not query issues. Error: Check that the GitHub secret used by this job has Sentry |
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (4)
packages/web/src/lib/task-funding/payments.server.ts-292-338 (1)
292-338: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUse the
CommercePaymentProviderenum here. The schema already definesCommercePaymentProviderwithSTRIPE, and this file can useCommercePaymentProvider.STRIPEinstead of the raw string literal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/lib/task-funding/payments.server.ts` around lines 292 - 338, The `tx.commerceOrder.create` call in `payments.server.ts` is using a raw payment provider string instead of the shared enum value. Update the `paymentProvider` field in this order creation path to use `CommercePaymentProvider.STRIPE` so it stays aligned with the schema and other payment-provider references. Use the existing `CommercePaymentProvider` symbol in this function rather than the string literal.Source: Coding guidelines
packages/web/src/app/fund/page.logged-out.md-21-56 (1)
21-56: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRegenerate the fund preview snapshot.
packages/web/src/app/fund/page.tsxalready renders the EV/funding-denominator and funding-status rows for every ranked task, sopackages/web/src/app/fund/page.logged-out.mdis out of sync.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/app/fund/page.logged-out.md` around lines 21 - 56, The fund preview snapshot is stale and no longer matches the rendered output from page.tsx. Regenerate packages/web/src/app/fund/page.logged-out.md from the current output of the fund page so it includes the EV/funding-denominator and funding-status rows for every ranked task, keeping the snapshot aligned with the page component’s current rendering.packages/web/src/lib/task-funding/status.server.ts-131-151 (1)
131-151: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAlign paid-person actor keys with pledge actor keys
pledges.server.tsstores people asperson:${user.person.id}, but paid payments are keyed here asuser:${payment.donorUserId}. That means the same person can be counted twice when they both pledge and pay. Normalize both paths to the same actor key scheme.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/lib/task-funding/status.server.ts` around lines 131 - 151, The actor key normalization in status.server.ts is inconsistent between active pledges and paid payments, causing the same person to be counted twice. Update the paid-payment actorKey construction in the payment loop to use the same person key scheme as pledges.server.ts and the pledge path, and keep the task-funding grouping logic in sync by adjusting the relevant status.server.ts actorKinds aggregation and any shared key-building code.packages/web/src/lib/stripe-connect.server.ts-209-237 (1)
209-237: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winPreserve the first onboarding completion timestamp.
onboardingCompletedAtis reset tonowon every sync after the account is active, which loses the actual completion time.Proposed timestamp fix
function mapAccountUpdateData( account: StripeV2Account, - existing?: Pick<StripeConnectedAccount, "onboardingStartedAt"> | null, + existing?: Pick< + StripeConnectedAccount, + "onboardingCompletedAt" | "onboardingStartedAt" + > | null, ) { @@ onboardingCompletedAt: - status === StripeConnectedAccountStatus.ONBOARDING_COMPLETE + status === StripeConnectedAccountStatus.ONBOARDING_COMPLETE && + !existing?.onboardingCompletedAt ? now : undefined,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/lib/stripe-connect.server.ts` around lines 209 - 237, The update mapping in mapAccountUpdateData is overwriting onboardingCompletedAt with the current sync time whenever the account is already ONBOARDING_COMPLETE. Preserve the first completion timestamp by only setting onboardingCompletedAt when it is currently unset, using existing?.onboardingCompletedAt as the source of truth and leaving it unchanged on later syncs. Keep the rest of the StripeConnectedAccount update fields in mapAccountUpdateData the same.
🧹 Nitpick comments (11)
packages/web/src/lib/__tests__/task-payouts.server.test.ts (1)
1-192: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftMissing test coverage for the Stripe-transfer/queueing/retry paths.
Only pure calculation helpers are tested.
executeTaskPayout,queueTaskPayoutForVerifiedClaim,reconcileQueuedPayoutReadiness, andretryDueTaskPayouts— the functions that actually move money and are most exposed to the concurrency issue flagged intask-payouts.server.ts— have no test coverage here. Given this is a payment-critical path, adding tests with a mocked Stripe client (success,balance_insufficient, and concurrent-claim scenarios) would meaningfully reduce regression risk.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/lib/__tests__/task-payouts.server.test.ts` around lines 1 - 192, Add coverage for the money-moving flow, not just the calculation helpers, by testing executeTaskPayout, queueTaskPayoutForVerifiedClaim, reconcileQueuedPayoutReadiness, and retryDueTaskPayouts from task-payouts.server. Use mocked Stripe behavior to cover a successful transfer, a balance_insufficient failure, and a concurrent-claim/queueing scenario so the retry and readiness logic is exercised. Keep the existing task-payouts.server.test.ts structure, but extend it with cases that verify these functions’ status transitions and queue behavior.packages/web/src/lib/task-payouts.server.ts (1)
55-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract Stripe rail identifiers instead of comparing raw string literals.
"stripe"/"stripe_connect"are compared as inline string literals for a business-critical eligibility gate. A typo here silently disables/enables Stripe payouts for a task's compensation rails.♻️ Suggested constant extraction
+const STRIPE_PAYMENT_RAILS = new Set(["stripe", "stripe_connect"]); + export function canUseStripeForTaskCompensation( task: Pick<FixedTaskPayoutInput, "compensationKind" | "compensationPaymentRails">, ) { ... return task.compensationPaymentRails.some((rail) => { const normalized = rail.trim().toLowerCase(); - return normalized === "stripe" || normalized === "stripe_connect"; + return STRIPE_PAYMENT_RAILS.has(normalized); }); }As per coding guidelines, "Use enums instead of magic strings in TypeScript code" (
**/*.{ts,tsx}).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/lib/task-payouts.server.ts` around lines 55 - 58, The Stripe eligibility check in task payout rail handling currently uses inline string literals for “stripe” and “stripe_connect”, which should be replaced with named identifiers. Extract these values into shared constants or an enum in task-payouts.server.ts and have the predicate in the rail-checking logic reference those symbols instead of comparing raw strings directly. Keep the normalization behavior intact while centralizing the Stripe rail identifiers for safer maintenance.Source: Coding guidelines
packages/web/scripts/guard-local-dev-db.mjs (1)
129-138: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winLive-key detection misses Stripe restricted keys (
rk_live_).Stripe's own guidance recommends restricted keys (prefixed
rk_live_/rk_test_) over the default secret key for server-side use. The current check only matchessk_live_/pk_live_, so a live restricted key would slip through this guard undetected.♻️ Proposed fix
].filter(([, value]) => { const key = String(value ?? "").trim(); - return key.startsWith("sk_live_") || key.startsWith("pk_live_"); + return ( + key.startsWith("sk_live_") || + key.startsWith("pk_live_") || + key.startsWith("rk_live_") + ); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/scripts/guard-local-dev-db.mjs` around lines 129 - 138, The live-key check in guard-local-dev-db.mjs only flags values starting with sk_live_ or pk_live_, so it misses Stripe restricted keys. Update the liveKeys filter to also treat rk_live_ as a live key, using the existing env entries and String(value ?? "").trim() normalization in the same guard. Keep the detection logic centralized in the liveKeys array/filter so the local-dev DB guard blocks all live Stripe keys consistently.packages/web/src/app/admin/task-payouts/page.tsx (1)
79-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a page title via
getRouteMetadata().This new page has no
metadata/generateMetadataexport, leaving the browser tab title unset. As per coding guidelines,packages/web/src/app/**/*.{ts,tsx}should "UsegetRouteMetadata()from routes.ts for page titles instead of hardcoding them," withroutes.tsas the source of truth for titles/descriptions. Verifyroutes.tshas (or add) an entry for this admin route and wire upexport const metadata = getRouteMetadata(...).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/app/admin/task-payouts/page.tsx` around lines 79 - 91, The AdminTaskPayoutsPage route is missing page metadata, so add a metadata export using getRouteMetadata() to set the browser tab title. Check routes.ts for an existing admin task payouts entry or add one there first, then wire export const metadata = getRouteMetadata(...) in AdminTaskPayoutsPage so the title comes from the shared route source of truth instead of being hardcoded.Source: Coding guidelines
packages/web/src/app/api/tasks/[id]/fund/checkout/route.ts (1)
50-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAuth errors silently swallowed.
getCurrentUser(request).catch(() => null)treats any error (including unexpected auth-config failures, not just "no session") as anonymous donor. Consider logging the caught error so real auth issues aren't masked as normal anonymous checkouts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/app/api/tasks/`[id]/fund/checkout/route.ts around lines 50 - 56, The anonymous checkout path in the task funding route is swallowing all errors from getCurrentUser(request), which can hide real auth failures. Update the createTaskFundingCheckoutSession flow to catch the error explicitly, log it with context before falling back to null donorUserId, and keep the existing anonymous behavior only for expected unauthenticated cases. Use the getCurrentUser and createTaskFundingCheckoutSession symbols to locate the change.packages/web/src/app/api/stripe/webhook/route.ts (1)
117-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRefund/dispute handlers aren't scoped by
order_typelike the other task-funding branches.
charge.refundedandcharge.dispute.createdunconditionally callrecordTaskFundingChargeRefunded/recordTaskFundingChargeDisputedfor every charge, relying ontaskFundingPayment.findFirstreturningnullfor unrelated charges (shirts, store offers, generic donations). This works today but is inconsistent with the explicitorder_type === TASK_FUNDING_ORDER_TYPEchecks used elsewhere in this switch, and does an extra DB round-trip for every non-task-funding refund/dispute. Since Stripe copies PaymentIntent metadata onto the resulting Charge,charge.metadata?.order_typeshould be available here too.♻️ Suggested consistency fix
case "charge.refunded": { - await recordTaskFundingChargeRefunded(event.data.object as Stripe.Charge); + const charge = event.data.object as Stripe.Charge; + if (charge.metadata?.order_type === TASK_FUNDING_ORDER_TYPE) { + await recordTaskFundingChargeRefunded(charge); + } break; } case "charge.dispute.created": { - await recordTaskFundingChargeDisputed( - event.data.object as Stripe.Dispute, - ); + const dispute = event.data.object as Stripe.Dispute; + // dispute.charge does not carry metadata directly; recordTaskFundingChargeDisputed + // already looks up by stripeChargeId, so this is safe to leave as-is if metadata isn't reliably present. + await recordTaskFundingChargeDisputed(dispute); break; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/app/api/stripe/webhook/route.ts` around lines 117 - 126, Scope the `charge.refunded` and `charge.dispute.created` cases in the webhook switch the same way as the other task-funding branches by checking `charge.metadata?.order_type === TASK_FUNDING_ORDER_TYPE` before calling `recordTaskFundingChargeRefunded` or `recordTaskFundingChargeDisputed`. Use the `event.data.object as Stripe.Charge` metadata directly in `route.ts` so unrelated refunds/disputes are skipped without hitting the task-funding lookup functions.packages/web/src/lib/task-funding/payments.server.ts (1)
371-412: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNo idempotency key on Stripe Checkout session creation.
If the client retries this call (network blip, double-click) before the first request completes, a second
CommerceOrder/TaskFundingPayment/Checkout Session gets created for the same intent. Stripe supports passing{ idempotencyKey }as a second argument tocreate().♻️ Suggested fix
const session = await stripe.checkout.sessions.create({ client_reference_id: input.donorUserId ?? undefined, ... cancel_url: `${baseUrl}${getTaskPath(task.id)}?funding_canceled=1`, - }); + }, { idempotencyKey: `task-funding-checkout-${payment.id}` });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/lib/task-funding/payments.server.ts` around lines 371 - 412, The Stripe Checkout session creation in the task funding flow is missing an idempotency key, which can create duplicate CommerceOrder, TaskFundingPayment, and session records on retries. Update the stripe.checkout.sessions.create call in the task funding payment logic to pass a stable idempotencyKey as the second argument, derived from the existing payment/order identifiers in this flow, so repeated requests for the same intent reuse the same Checkout Session.packages/web/src/app/api/stripe/webhook/route.test.ts (1)
177-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winOnly the happy-path task-funding case is tested.
New handlers were added for
checkout.session.async_payment_failed,checkout.session.expired,charge.refunded, andcharge.dispute.created, but onlycheckout.session.completed(paid) has a test. Given this is payment-critical logic, consider mirroring this test pattern for the failure/refund/dispute branches to lock in the status mapping (FAILEDvsCANCELED, refund/dispute recorder calls).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/app/api/stripe/webhook/route.test.ts` around lines 177 - 205, The webhook test coverage only exercises the task_funding success path, so add matching tests in the route test suite for the new handlers on checkout.session.async_payment_failed, checkout.session.expired, charge.refunded, and charge.dispute.created. Use the existing POST(makeWebhookRequest()) and mocks pattern around recordTaskFundingCheckoutPaid to verify the correct task-funding status mapping (FAILED vs CANCELED) and that the refund/dispute recorder functions are called for the corresponding charge events.packages/web/src/app/fund/page.logged-out.md (1)
22-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win"funding denominator" leaks implementation/planning language into public copy.
This phrase reads like an internal calculation term rather than something meant for a donor. Per the copy guideline, implementation/planning terms shouldn't leak into user-facing text. This snapshot reflects whatever
fund/page.tsxrenders, so the fix belongs there (and this.logged-out.mdfile should then be regenerated withpnpm --filter@optimitron/webcopy:preview, not hand-edited).Based on learnings/guidelines: "Do not leak implementation or planning terms into user-facing copy" and "Never hand-edit generated
page.logged-out.mdsnapshot files; regenerate them withpnpm --filter@optimitron/webcopy:preview."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/app/fund/page.logged-out.md` at line 22, The logged-out fund copy is leaking implementation language by showing “funding denominator” in user-facing text. Update the source rendering in fund/page.tsx to replace that internal calculation phrase with donor-friendly copy, then regenerate the snapshot in page.logged-out.md using pnpm --filter `@optimitron/web` copy:preview rather than editing the snapshot directly.Source: Coding guidelines
packages/web/src/app/tasks/[id]/page.tsx (1)
181-184: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace magic rail strings with a named constant.
"stripe"/"stripe_connect"are inlined string literals. As per coding guidelines,**/*.{ts,tsx}should "Use enums instead of magic strings in TypeScript code."♻️ Proposed fix
+const STRIPE_PAYMENT_RAILS = new Set(["stripe", "stripe_connect"]); + function isFixedStripePaidTask(task: { ... return task.compensationPaymentRails.some((rail) => { const normalized = rail.trim().toLowerCase(); - return normalized === "stripe" || normalized === "stripe_connect"; + return STRIPE_PAYMENT_RAILS.has(normalized); }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/app/tasks/`[id]/page.tsx around lines 181 - 184, The task rail check in the task page uses inline magic strings for Stripe rails, so replace the literal comparisons in the `task.compensationPaymentRails.some(...)` logic with a named enum or constant defined near the relevant task/payment rail types. Update the `page.tsx` matching logic to reference that shared symbol instead of `"stripe"` and `"stripe_connect"` so the rail values are centralized and type-safe.Source: Coding guidelines
packages/web/src/components/tasks/StripeConnectStatusPanel.tsx (1)
99-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the RetroUI Button primitive for both actions. Replace the hand-rolled styles on the Sign in and Set up payouts controls with
<Button asChild>so these standard actions stay consistent with the rest of the app.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/components/tasks/StripeConnectStatusPanel.tsx` around lines 99 - 103, The Sign in and Set up payouts controls in StripeConnectStatusPanel are still using hand-rolled anchor styling instead of the shared RetroUI Button primitive. Update both actions to use Button asChild, keeping the existing href/action behavior while moving the visible styling and interaction states into the Button component so they match the rest of the app.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/db/prisma/migrations/20260701120000_add_task_funding_payments_and_payouts/migration.sql`:
- Around line 25-26: Add database-level validation in this migration so the
funding payment and payout tables reject zero or negative values for amountCents
at insert/update time. Update the affected CREATE/ALTER TABLE definitions in the
migration to include an appropriate CHECK constraint on amountCents, and make
sure the same constraint is applied to both ledger tables referenced by this
migration.
In `@packages/db/prisma/schema.prisma`:
- Around line 6617-6648: TaskPayout currently allows taskClaimId, taskId, and
payeeUserId to drift independently, so a payout can reference mismatched
claim/task/user data. Update the TaskPayout model in the Prisma schema to
enforce alignment between the taskClaim relation and the taskId/payeeUserId
fields, either with a composite constraint or equivalent validation, using the
existing TaskPayout, taskClaim, task, and payeeUser relations as the anchor
points.
- Around line 6557-6560: The TaskFundingPayment/TaskPayout relation definitions
currently cascade-delete history when Task, CommerceOrder, or User rows are
removed, which breaks durability for payment/payout records. Update the Prisma
relation settings on the affected model fields (such as task, commerceOrder, and
any required anchor relations) to use Restrict instead of Cascade, and keep
SetNull only on optional snapshot fields like donorUser. Make the change
consistently in the schema around the TaskFundingPayment and TaskPayout relation
blocks so historic ledger rows remain preserved.
- Around line 6526-6559: The TaskFundingPayment schema currently allows targetId
to reference a target from a different task, so the task/target pair can become
inconsistent. Update the TaskFundingPayment model in schema.prisma to add a
composite relation on (targetId, taskId) against TaskFundingTarget so both
fields must match, and keep the existing single-field relations (task and
target) aligned with that constraint.
In `@packages/web/scripts/guard-local-dev-db.mjs`:
- Around line 85-101: The remote-database override in guard-local-dev-db.mjs
exits before the live Stripe check runs, so OPTIMITRON_ALLOW_REMOTE_DEV_DATABASE
currently disables guardLiveStripe entirely. Update the script flow so
guardLiveStripe(env) is invoked independently of the database URL branch and
before any early process.exit(0), while keeping isLocalDatabaseUrl only for the
local-db validation path. Use the existing guardLiveStripe and
isLocalDatabaseUrl helpers to preserve the current behavior for each check
without letting one override skip the other.
In `@packages/web/src/app/api/stripe/connect/account/route.ts`:
- Around line 21-28: The Stripe Connect account route is leaking internal thrown
messages to the browser by returning error.message in the NextResponse.json
payload. Update the handler in the route.ts logic for the account creation path
to log the detailed error server-side, but always return the existing stable
generic public message for connect creation failures instead of any lower-layer
Error text.
In `@packages/web/src/app/api/stripe/connect/onboarding-link/route.ts`:
- Around line 9-12: The onboarding link URL validation currently accepts any
HTTPS domain via OnboardingLinkBodySchema, which can mint redirects to untrusted
sites. Update the route handler in route.ts to validate refreshUrl and returnUrl
against trusted origins only, using the current app origin and/or getBaseUrl()
as the allowlist. Keep the Zod schema for basic parsing, but add an origin check
in the onboarding link creation flow before passing URLs through to Stripe.
In `@packages/web/src/app/fund/page.tsx`:
- Around line 55-66: The funding list ranking in the `rankFundingTasks()` flow
is still based on total target/cost via `getFundingDenominatorCents`, so fully
funded tasks can outrank tasks that still need money and later show an incorrect
“Fund this task” CTA. Update the ranking/slicing logic in the `fund` page to use
`getTaskFundingStatus()` before selecting the top tasks, and either filter out
tasks whose `remainingUsdCents` is 0 or include remaining funding need in the
ranking score so under-funded tasks are prioritized.
In `@packages/web/src/components/tasks/StripeConnectStatusPanel.tsx`:
- Around line 35-48: The StripeConnectStatusPanel useEffect only fetches the
cached status and never triggers a backend sync after a Stripe return, so update
the status request in StripeConnectStatusPanel to include sync=1 when
appropriate (for example when the panel is reached after stripe_connect=return)
and keep using the fetched payload to setStatus. Also handle non-OK fetch
responses explicitly in the same effect, not just network errors, so failed API
responses set the error state instead of silently parsing JSON.
In `@packages/web/src/lib/stripe-connect.server.ts`:
- Around line 87-99: Add a timeout to Stripe API calls made by stripeV2Request
so onboarding, status-sync, and payout flows don’t hang indefinitely on a slow
connection. Wrap the fetch in an AbortController-based timeout (or equivalent
request timeout handling), ensure the request is aborted when the limit is
exceeded, and surface a clear timeout error from stripeV2Request so callers can
handle it.
- Around line 276-371: The creation flow in getOrCreateStripeConnectedAccount is
not fully idempotent because stripeV2Request("/v2/core/accounts") runs before
any DB race handling, so concurrent calls can create duplicate external
accounts. Add a deterministic Stripe idempotency key to the account creation
request and update the existing-user path to catch the unique constraint race on
stripeConnectedAccount/userId, then requery and return the persisted row instead
of throwing. Keep the fix centered around getOrCreateStripeConnectedAccount,
stripeV2Request, and the db.stripeConnectedAccount create/update logic.
- Around line 101-105: Sanitize the failure path in stripeV2Request so Stripe’s
response body is not embedded in the thrown Error message. Keep the detailed
body only in server-side logging, and make the thrown error from stripeV2Request
a stable public message that the Connect handlers can safely forward without
exposing provider internals or account details.
In `@packages/web/src/lib/task-funding/payments.server.ts`:
- Around line 111-121: The funding target logic in chooseTargetAmountCents and
taskFundingTarget.upsert is incorrectly using checkoutAmountCents as a fallback,
which lets the first donor permanently set the task’s target. Remove that
fallback and require a real cost basis (compensationMaxAmountMinorUnits or
estimatedCashCostUsdBase) before proceeding; if neither exists, throw the “Task
has no funding target yet” error from the checkout flow. Also make sure
taskFundingTarget.upsert only persists a target derived from task data, not from
the current donation amount.
In `@packages/web/src/lib/task-payouts.server.ts`:
- Around line 512-530: The retry selector in retryDueTaskPayouts currently skips
TaskPayoutStatus.PROCESSING, which can leave stalled payouts unrecoverable.
Update the status filter in retryDueTaskPayouts to include PROCESSING and add a
stale-processing guard using the existing processing timestamp fields (for
example, only retry PROCESSING rows whose processingAt is older than a safe
threshold). Keep the existing ordering and limit behavior intact, and make sure
the recovery logic still works with the idempotency key used in the payout flow.
- Around line 81-185: The funding check and payout state transitions in
getAvailableTaskFundingCents, executeTaskPayout,
queueTaskPayoutForVerifiedClaim, and reconcileQueuedPayoutReadiness are not
protected against concurrent task-level writes, so two callers can allocate the
same funding snapshot. Fix this by serializing all readiness/allocation work per
taskId: perform the funding read plus status upsert/transition inside one
prisma.$transaction with Serializable isolation, or acquire a Postgres advisory
lock keyed on taskId before computing availability and committing the payout
state.
- Around line 61-79: Update getFixedTaskPayoutAmountCents in
task-payouts.server.ts so it only returns a fixed payout when
compensationCadence is explicitly set to TaskCompensationCadence.FIXED. Treat a
null/undefined cadence as unsupported by returning null, and keep the existing
currency/amount validation intact. This will prevent
queueTaskPayoutForVerifiedClaim from auto-queuing Stripe payouts for tasks
without an explicit fixed cadence.
In `@packages/web/src/lib/tasks.server.ts`:
- Around line 2077-2091: Payout creation can be lost when
queueTaskPayoutForVerifiedClaim throws after a claim is marked VERIFIED, because
the current catch in tasks.server.ts only logs the failure and
retryDueTaskPayouts does not pick up missing TaskPayout rows. Update the
reconciliation flow so a periodic job also scans for VERIFIED claims on eligible
tasks that չունe a corresponding TaskPayout record, then re-invokes
queueTaskPayoutForVerifiedClaim for those claims; use the existing
queueTaskPayoutForVerifiedClaim, retryDueTaskPayouts, and the claim-verification
path in tasks.server.ts as the integration points.
---
Minor comments:
In `@packages/web/src/app/fund/page.logged-out.md`:
- Around line 21-56: The fund preview snapshot is stale and no longer matches
the rendered output from page.tsx. Regenerate
packages/web/src/app/fund/page.logged-out.md from the current output of the fund
page so it includes the EV/funding-denominator and funding-status rows for every
ranked task, keeping the snapshot aligned with the page component’s current
rendering.
In `@packages/web/src/lib/stripe-connect.server.ts`:
- Around line 209-237: The update mapping in mapAccountUpdateData is overwriting
onboardingCompletedAt with the current sync time whenever the account is already
ONBOARDING_COMPLETE. Preserve the first completion timestamp by only setting
onboardingCompletedAt when it is currently unset, using
existing?.onboardingCompletedAt as the source of truth and leaving it unchanged
on later syncs. Keep the rest of the StripeConnectedAccount update fields in
mapAccountUpdateData the same.
In `@packages/web/src/lib/task-funding/payments.server.ts`:
- Around line 292-338: The `tx.commerceOrder.create` call in
`payments.server.ts` is using a raw payment provider string instead of the
shared enum value. Update the `paymentProvider` field in this order creation
path to use `CommercePaymentProvider.STRIPE` so it stays aligned with the schema
and other payment-provider references. Use the existing
`CommercePaymentProvider` symbol in this function rather than the string
literal.
In `@packages/web/src/lib/task-funding/status.server.ts`:
- Around line 131-151: The actor key normalization in status.server.ts is
inconsistent between active pledges and paid payments, causing the same person
to be counted twice. Update the paid-payment actorKey construction in the
payment loop to use the same person key scheme as pledges.server.ts and the
pledge path, and keep the task-funding grouping logic in sync by adjusting the
relevant status.server.ts actorKinds aggregation and any shared key-building
code.
---
Nitpick comments:
In `@packages/web/scripts/guard-local-dev-db.mjs`:
- Around line 129-138: The live-key check in guard-local-dev-db.mjs only flags
values starting with sk_live_ or pk_live_, so it misses Stripe restricted keys.
Update the liveKeys filter to also treat rk_live_ as a live key, using the
existing env entries and String(value ?? "").trim() normalization in the same
guard. Keep the detection logic centralized in the liveKeys array/filter so the
local-dev DB guard blocks all live Stripe keys consistently.
In `@packages/web/src/app/admin/task-payouts/page.tsx`:
- Around line 79-91: The AdminTaskPayoutsPage route is missing page metadata, so
add a metadata export using getRouteMetadata() to set the browser tab title.
Check routes.ts for an existing admin task payouts entry or add one there first,
then wire export const metadata = getRouteMetadata(...) in AdminTaskPayoutsPage
so the title comes from the shared route source of truth instead of being
hardcoded.
In `@packages/web/src/app/api/stripe/webhook/route.test.ts`:
- Around line 177-205: The webhook test coverage only exercises the task_funding
success path, so add matching tests in the route test suite for the new handlers
on checkout.session.async_payment_failed, checkout.session.expired,
charge.refunded, and charge.dispute.created. Use the existing
POST(makeWebhookRequest()) and mocks pattern around
recordTaskFundingCheckoutPaid to verify the correct task-funding status mapping
(FAILED vs CANCELED) and that the refund/dispute recorder functions are called
for the corresponding charge events.
In `@packages/web/src/app/api/stripe/webhook/route.ts`:
- Around line 117-126: Scope the `charge.refunded` and `charge.dispute.created`
cases in the webhook switch the same way as the other task-funding branches by
checking `charge.metadata?.order_type === TASK_FUNDING_ORDER_TYPE` before
calling `recordTaskFundingChargeRefunded` or `recordTaskFundingChargeDisputed`.
Use the `event.data.object as Stripe.Charge` metadata directly in `route.ts` so
unrelated refunds/disputes are skipped without hitting the task-funding lookup
functions.
In `@packages/web/src/app/api/tasks/`[id]/fund/checkout/route.ts:
- Around line 50-56: The anonymous checkout path in the task funding route is
swallowing all errors from getCurrentUser(request), which can hide real auth
failures. Update the createTaskFundingCheckoutSession flow to catch the error
explicitly, log it with context before falling back to null donorUserId, and
keep the existing anonymous behavior only for expected unauthenticated cases.
Use the getCurrentUser and createTaskFundingCheckoutSession symbols to locate
the change.
In `@packages/web/src/app/fund/page.logged-out.md`:
- Line 22: The logged-out fund copy is leaking implementation language by
showing “funding denominator” in user-facing text. Update the source rendering
in fund/page.tsx to replace that internal calculation phrase with donor-friendly
copy, then regenerate the snapshot in page.logged-out.md using pnpm --filter
`@optimitron/web` copy:preview rather than editing the snapshot directly.
In `@packages/web/src/app/tasks/`[id]/page.tsx:
- Around line 181-184: The task rail check in the task page uses inline magic
strings for Stripe rails, so replace the literal comparisons in the
`task.compensationPaymentRails.some(...)` logic with a named enum or constant
defined near the relevant task/payment rail types. Update the `page.tsx`
matching logic to reference that shared symbol instead of `"stripe"` and
`"stripe_connect"` so the rail values are centralized and type-safe.
In `@packages/web/src/components/tasks/StripeConnectStatusPanel.tsx`:
- Around line 99-103: The Sign in and Set up payouts controls in
StripeConnectStatusPanel are still using hand-rolled anchor styling instead of
the shared RetroUI Button primitive. Update both actions to use Button asChild,
keeping the existing href/action behavior while moving the visible styling and
interaction states into the Button component so they match the rest of the app.
In `@packages/web/src/lib/__tests__/task-payouts.server.test.ts`:
- Around line 1-192: Add coverage for the money-moving flow, not just the
calculation helpers, by testing executeTaskPayout,
queueTaskPayoutForVerifiedClaim, reconcileQueuedPayoutReadiness, and
retryDueTaskPayouts from task-payouts.server. Use mocked Stripe behavior to
cover a successful transfer, a balance_insufficient failure, and a
concurrent-claim/queueing scenario so the retry and readiness logic is
exercised. Keep the existing task-payouts.server.test.ts structure, but extend
it with cases that verify these functions’ status transitions and queue
behavior.
In `@packages/web/src/lib/task-funding/payments.server.ts`:
- Around line 371-412: The Stripe Checkout session creation in the task funding
flow is missing an idempotency key, which can create duplicate CommerceOrder,
TaskFundingPayment, and session records on retries. Update the
stripe.checkout.sessions.create call in the task funding payment logic to pass a
stable idempotencyKey as the second argument, derived from the existing
payment/order identifiers in this flow, so repeated requests for the same intent
reuse the same Checkout Session.
In `@packages/web/src/lib/task-payouts.server.ts`:
- Around line 55-58: The Stripe eligibility check in task payout rail handling
currently uses inline string literals for “stripe” and “stripe_connect”, which
should be replaced with named identifiers. Extract these values into shared
constants or an enum in task-payouts.server.ts and have the predicate in the
rail-checking logic reference those symbols instead of comparing raw strings
directly. Keep the normalization behavior intact while centralizing the Stripe
rail identifiers for safer maintenance.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dc7634c8-067a-48e1-9df6-0fffdc8016e7
📒 Files selected for processing (31)
docs/LOCAL_DB.mdpackages/db/prisma/migrations/20260701120000_add_task_funding_payments_and_payouts/migration.sqlpackages/db/prisma/schema.prismapackages/web/package.jsonpackages/web/scripts/guard-local-dev-db.mjspackages/web/src/app/admin/task-payouts/page.tsxpackages/web/src/app/api/cron/task-payouts/route.tspackages/web/src/app/api/stripe/connect/account/route.tspackages/web/src/app/api/stripe/connect/onboarding-link/route.tspackages/web/src/app/api/stripe/connect/status/route.tspackages/web/src/app/api/stripe/webhook/route.test.tspackages/web/src/app/api/stripe/webhook/route.tspackages/web/src/app/api/tasks/[id]/fund/checkout/route.tspackages/web/src/app/api/tasks/[id]/funding-status/route.tspackages/web/src/app/api/tasks/[id]/pledge/route.tspackages/web/src/app/fund/page.logged-out.mdpackages/web/src/app/fund/page.tsxpackages/web/src/app/tasks/[id]/page.tsxpackages/web/src/components/task-funding/TaskFundingCheckoutForm.tsxpackages/web/src/components/task-funding/TaskFundingProgress.tsxpackages/web/src/components/task-funding/__tests__/TaskFundingProgress.test.tsxpackages/web/src/components/tasks/StripeConnectStatusPanel.tsxpackages/web/src/lib/__tests__/task-payouts.server.test.tspackages/web/src/lib/routes.tspackages/web/src/lib/stripe-connect.server.tspackages/web/src/lib/task-funding/__tests__/status.server.test.tspackages/web/src/lib/task-funding/payments.server.tspackages/web/src/lib/task-funding/status.server.tspackages/web/src/lib/task-payouts.server.tspackages/web/src/lib/tasks.server.tspackages/web/vercel.json
Drop the Codex-dispatch protocol and its enforcement: - Delete .claude/codex-delegation.md - Delete hooks: block-codex-rescue-agent, codex-dispatch-blather, enforce-codex-protocol, enforce-codex-background, enforce-manual-search-in-copy-dispatch, enforce-audience-and-goal-on-ui-dispatch - Remove their registrations from .claude/settings.json - Remove the qa-passed pre-commit gate (it required a Codex preflight) - Drop the Codex-delegation section + dispatch note from CLAUDE.md Keeps enforce-copy-review-before-commit (Mike's copy before/after gate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rewrite /fund hero in Wishonia voice: lead with ending war and disease, frame the ranked tasks as a price list (Nothing proven, nothing paid) - Rank /fund by best estimate with title dedupe and funding-source labels (funding goal vs worker payout); drop cost-estimate fallback, cap at 12 - Gate paid-task claims behind Stripe payout setup via #payouts anchor - Reorder task page so the payout panel precedes the checkout form - Clarify Stripe Connect status, claim button, and admin payout copy - Regenerate /fund and /admin/task-payouts copy snapshots todo-touched: Parked "Donate-to-fund-task marketplace, Stripe Connect disbursement" — narrowed now that both landed here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#97) - guard-local-dev-db.mjs: run guardLiveStripe unconditionally before the remote-DB early exit. OPTIMITRON_ALLOW_REMOTE_DEV_DATABASE=1 previously process.exit(0)'d before the live-Stripe-key check ever ran, so opting into a remote DB silently disabled the independent Stripe guard. - stripe-connect.server.ts: stripeV2Request no longer embeds Stripe's response body in the thrown message. Log the body server-side; throw a stable public string so Connect handlers that forward error.message can't leak provider internals or account details. - connect/account/route.ts: log the error server-side and return the existing generic message instead of echoing error.message to the browser. - connect/onboarding-link/route.ts: validate client-supplied refreshUrl/returnUrl against trusted origins (getBaseUrl + request origin). z.string().url() alone allowed any HTTPS host, an open-redirect via the Stripe onboarding return. todo-skipped: net-new payout/donation feature hardening, no TODO line covers it Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- payments.server.ts: chooseTargetAmountCents no longer falls back to the current donation amount. taskFundingTarget.upsert only writes targetAmountCents on create, so the first donor's checkout amount was permanently pinned as the target (a $5 test donation -> $5 target the page reports "100% funded"). Require a real cost basis (compensationMaxAmountMinorUnits or estimatedCashCostUsdBase); throw "Task has no funding target yet." otherwise. - status.server.ts: per-unit usd pledgerCount counted raw paid-payment rows, over-counting when one donor pays multiple times and diverging from the de-duped top-level status.pledgerCount. Count distinct paid-payment actor keys. - task-payouts.server.ts: getFixedTaskPayoutAmountCents required only that cadence != non-FIXED, so a PAID task with a null cadence auto-queued a full-amount transfer. Require cadence === FIXED explicitly. - task-payouts.server.ts: retryDueTaskPayouts now recovers PROCESSING payouts older than STALE_PROCESSING_MS (orphaned by a crash/timeout between the PROCESSING flip and persisting the Stripe response). Safe because the transfer uses a deterministic idempotency key. todo-skipped: net-new payout/donation feature correctness, no TODO line covers it Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capture the CodeRabbit-flagged payout concurrency / reconciliation / idempotency / timeout items and the schema-boundary DB invariants (CHECK constraints, composite relations, cascade->restrict) as one tracked follow-up. All are real but heavy or schema-coupled; the schema pieces need the coordinated schema-only migration + Mike's approval, so they don't belong as mid-PR patches on an already-applied migration. todo-touched: Task-payout ledger hardening (deferred from PR #97) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Throw on non-2xx /api/stripe/connect/status so a 401 no longer sticks forever on "Checking payout setup" - Request ?sync=1 when returning from onboarding so a stale cached status doesn't keep payouts pending and block paid-task claiming Resolves Copilot PR #97 threads on StripeConnectStatusPanel. todo-skipped: bot-comment fix, no TODO item Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Concurrent verified claims on one task could each read available funding and both transition to READY/PROCESSING, transferring the same funds twice. - Serialize each read+transition per task with a Postgres transaction-level advisory lock (withTaskFundingLock) in queue/execute/reconcile; the Stripe transfer stays outside the lock so a slow network call never holds the row - Reserve only committed states (READY/PROCESSING/TRANSFERRED) in getAvailableTaskFundingCents so competing claims resolve first-come instead of mutually blocking forever - Add a concurrency test: two racing claims on a single-funded task settle to exactly one payout, funding never over-allocated Closes PR #97 thread 21 (CodeRabbit CRITICAL). todo-touched: Task-payout ledger hardening — critical double-allocation race fixed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
CommerceOrderand a paidTaskFundingPaymentledger./fundwith an EV-ranked task funding marketplace and add task-page funding controls/progress.Reviewed routes / states
/fund?logout=1/tasks/lab-grant-xai-2026-q3?logout=1#funding/tasks/1-pct-treaty?logout=1#funding/tasks/shirt-seed?login=demo#funding/admin/task-payouts?login=demoScreenshots were captured and inspected locally for desktop and mobile. One mobile admin layout issue was found and fixed before this PR.
Test plan
pnpm --filter @optimitron/db run prisma:validatepnpm db:deployagainst local Docker Postgrespnpm db:sync:managed-data -- --applypnpm --filter @optimitron/db run buildpnpm --filter @optimitron/web run typecheck:apppnpm --filter @optimitron/web run typecheck:testspnpm --filter @optimitron/web exec vitest run src/app/api/stripe/webhook/route.test.ts src/lib/task-funding/__tests__/status.server.test.ts src/components/task-funding/__tests__/TaskFundingProgress.test.tsx src/lib/__tests__/task-payouts.server.test.tspnpm --filter @optimitron/web copy:preview -- --routes=/fundgit diff --checkNotes
Summary by CodeRabbit