Skip to content

feat: add task donations and connect payouts#97

Open
mikepsinn wants to merge 8 commits into
mainfrom
feature/task-donations-connect-payouts
Open

feat: add task donations and connect payouts#97
mikepsinn wants to merge 8 commits into
mainfrom
feature/task-donations-connect-payouts

Conversation

@mikepsinn

@mikepsinn mikepsinn commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add task donation checkout backed by CommerceOrder and a paid TaskFundingPayment ledger.
  • Add Stripe Connect recipient onboarding/status APIs for payable task workers.
  • Add automated task payout ledger and retry cron for verified paid task work.
  • Replace /fund with an EV-ranked task funding marketplace and add task-page funding controls/progress.
  • Add local dev safety guards so local web dev refuses remote production DB URLs and live Stripe keys by default.

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=demo

Screenshots 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:validate
  • pnpm db:deploy against local Docker Postgres
  • pnpm db:sync:managed-data -- --apply
  • pnpm --filter @optimitron/db run build
  • pnpm --filter @optimitron/web run typecheck:app
  • pnpm --filter @optimitron/web run typecheck:tests
  • pnpm --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.ts
  • pnpm --filter @optimitron/web copy:preview -- --routes=/fund
  • git diff --check

Notes

  • Full live checkout/Connect transfer testing should use Stripe test-mode keys locally. The local dev guard intentionally blocks live Stripe keys unless explicitly overridden.

Summary by CodeRabbit

  • New Features
    • Added task funding checkout and updated funding progress/status with supporter counts (paid + pledged).
    • Added Stripe Connect onboarding/status UI and payout setup gating for paid-claim actions.
    • Added an admin “task payouts” page and automated payout retries via cron.
    • Updated the “Fund Tasks” landing page and related navigation copy.
  • Bug Fixes
    • Extended Stripe webhook handling for task-funding completion, refunds, and disputes.
    • Added local development safeguards for database and live Stripe key usage.
  • Documentation
    • Clarified local development environment and preview-data workflow guidance.

Copilot AI review requested due to automatic review settings July 2, 2026 00:05
@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
optimitron-web Ready Ready Preview, Comment Jul 2, 2026 5:06am

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mikepsinn, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e85e638e-5060-4683-96c9-8bbe79e3ec64

📥 Commits

Reviewing files that changed from the base of the PR and between 3d8aaa1 and 6b73d31.

📒 Files selected for processing (10)
  • TODO.md
  • packages/web/scripts/guard-local-dev-db.mjs
  • packages/web/src/app/api/stripe/connect/account/route.ts
  • packages/web/src/app/api/stripe/connect/onboarding-link/route.ts
  • packages/web/src/components/tasks/StripeConnectStatusPanel.tsx
  • packages/web/src/lib/__tests__/task-payouts.server.test.ts
  • packages/web/src/lib/stripe-connect.server.ts
  • packages/web/src/lib/task-funding/payments.server.ts
  • packages/web/src/lib/task-funding/status.server.ts
  • packages/web/src/lib/task-payouts.server.ts
📝 Walkthrough

Walkthrough

Adds 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.

Changes

Task Funding & Payout System

Layer / File(s) Summary
Schema and migration
packages/db/prisma/schema.prisma, packages/db/prisma/migrations/20260701120000_add_task_funding_payments_and_payouts/migration.sql
Adds funding/payment/payout enums, tables, indexes, foreign keys, and Prisma relations across task, order, user, person, and organization models.
Stripe Connect account flows
packages/web/src/lib/stripe-connect.server.ts, packages/web/src/app/api/stripe/connect/*, packages/web/src/components/tasks/StripeConnectStatusPanel.tsx
Adds Stripe Connect account sync/create/onboarding logic, API routes for status and onboarding, and the client panel that fetches status and starts onboarding.
Task funding checkout and webhook recording
packages/web/src/lib/task-funding/payments.server.ts, packages/web/src/lib/task-funding/status.server.ts, packages/web/src/app/api/tasks/[id]/fund/checkout/route.ts, packages/web/src/app/api/stripe/webhook/route.ts, packages/web/src/app/api/stripe/webhook/route.test.ts, packages/web/src/app/api/tasks/[id]/{funding-status,pledge}/route.ts
Creates funding checkout sessions, records payment lifecycle events from Stripe webhooks, and extends funding-status responses with paid and pledged totals.
Payout execution and retry orchestration
packages/web/src/lib/task-payouts.server.ts, packages/web/src/lib/tasks.server.ts, packages/web/src/app/api/cron/task-payouts/route.ts, packages/web/vercel.json, packages/web/src/lib/__tests__/task-payouts.server.test.ts
Computes payout amounts and available funding, queues and executes payouts, retries due payouts on cron, and wires the flow into task claim/verification.
Task and fund pages
packages/web/src/app/tasks/[id]/page.tsx, packages/web/src/app/fund/page.tsx, packages/web/src/app/fund/page.logged-out.md, packages/web/src/components/task-funding/*, packages/web/src/lib/routes.ts
Updates the task detail funding area, task claim CTA, checkout form, funding progress copy, and the ranked fund page plus its logged-out content.
Admin view, dev guard, and docs
packages/web/src/app/admin/task-payouts/page.tsx, packages/web/src/app/admin/task-payouts/page.logged-out.md, packages/web/scripts/guard-local-dev-db.mjs, packages/web/package.json, docs/LOCAL_DB.md, .claude/*, CLAUDE.md, TODO.md
Adds the admin payout list, local-dev DB/Stripe guard, script wiring, documentation updates, and hook/settings cleanup.
Estimated code review effort: 4 (Complex) ~75 minutes

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
Loading
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
Loading

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • mikepsinn/optimitron#84 — Touches the same .claude/hooks/enforce-codex-background.mjs hook file that this PR removes.
  • mikepsinn/optimitron#86 — Also extends packages/web/src/app/api/stripe/webhook/route.ts with new Stripe event branches.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main addition of task donations plus Stripe Connect payout support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/task-donations-connect-payouts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 TaskPayout queuing/execution/retry via cron and an admin payout backlog page.
  • Add local dev guard to block non-local DATABASE_URL and live Stripe keys by default; update /fund navigation + 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.

Comment thread packages/web/src/lib/task-funding/status.server.ts
Comment thread packages/web/src/components/tasks/StripeConnectStatusPanel.tsx Outdated
Comment thread packages/web/scripts/guard-local-dev-db.mjs
Comment thread packages/web/src/app/fund/page.tsx Outdated
Comment thread packages/web/src/app/fund/page.tsx
Comment on lines +734 to +741
enum TaskFundingPaymentStatus {
PENDING
PAID
FAILED
CANCELED
REFUNDED
DISPUTED
}
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

PR review packet

Start here

  • 🖼️ Visual review
  • 🚀 Preview deployment
  • ☝️ Cmd/Ctrl-click review links to keep this PR open.
  • 🔑 ?login=demo signs in as the demo user; ?logout=1 clears the session.
  • 💬 For a visual problem, use the comment button in latest.html or reply here with @claude and the checklist item.

Review checklist

Changed files considered
  • .claude/hooks/verify-ui-changes.mjs
  • .claude/settings.json
  • CLAUDE.md
  • TODO.md
  • docs/LOCAL_DB.md
  • packages/db/prisma/migrations/20260701120000_add_task_funding_payments_and_payouts/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/package.json
  • packages/web/scripts/guard-local-dev-db.mjs
  • packages/web/src/app/admin/task-payouts/page.logged-out.md
  • packages/web/src/app/admin/task-payouts/page.tsx
  • packages/web/src/app/api/cron/task-payouts/route.ts
  • packages/web/src/app/api/stripe/connect/account/route.ts
  • packages/web/src/app/api/stripe/connect/onboarding-link/route.ts
  • packages/web/src/app/api/stripe/connect/status/route.ts
  • packages/web/src/app/api/stripe/webhook/route.test.ts
  • packages/web/src/app/api/stripe/webhook/route.ts
  • packages/web/src/app/api/tasks/[id]/fund/checkout/route.ts
  • packages/web/src/app/api/tasks/[id]/funding-status/route.ts
  • packages/web/src/app/api/tasks/[id]/pledge/route.ts
  • packages/web/src/app/fund/page.logged-out.md
  • packages/web/src/app/fund/page.tsx
  • packages/web/src/app/tasks/[id]/page.tsx
  • packages/web/src/components/task-funding/TaskFundingCheckoutForm.tsx
  • packages/web/src/components/task-funding/TaskFundingProgress.tsx
  • packages/web/src/components/task-funding/__tests__/TaskFundingProgress.test.tsx
  • packages/web/src/components/tasks/StripeConnectStatusPanel.tsx
  • packages/web/src/components/tasks/TaskClaimButton.tsx
  • packages/web/src/lib/__tests__/task-payouts.server.test.ts
  • packages/web/src/lib/routes.ts
  • packages/web/src/lib/stripe-connect.server.ts
  • packages/web/src/lib/task-funding/__tests__/status.server.test.ts
  • packages/web/src/lib/task-funding/payments.server.ts
  • packages/web/src/lib/task-funding/status.server.ts
  • packages/web/src/lib/task-payouts.server.ts
  • packages/web/src/lib/tasks.server.ts
  • packages/web/vercel.json

Updated automatically when this PR's preview or visual review reruns.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Sentry preview audit failed

The preview smoke test ran, but the Sentry audit could not query issues.

Error: Sentry API returned HTTP 401 for https://sentry.io/api/0/projects/wishonia-org/optimitron-web/issues/?environment=vercel-preview&limit=50&query=is%3Aunresolved&sort=date&statsPeriod=24h: {"detail":"Token expired"}

Check that the GitHub secret used by this job has Sentry org:read, project:read, and event:read scopes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

Use the CommercePaymentProvider enum here. The schema already defines CommercePaymentProvider with STRIPE, and this file can use CommercePaymentProvider.STRIPE instead 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 win

Regenerate the fund preview snapshot. packages/web/src/app/fund/page.tsx already renders the EV/funding-denominator and funding-status rows for every ranked task, so packages/web/src/app/fund/page.logged-out.md is 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 win

Align paid-person actor keys with pledge actor keys

pledges.server.ts stores people as person:${user.person.id}, but paid payments are keyed here as user:${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 win

Preserve the first onboarding completion timestamp.

onboardingCompletedAt is reset to now on 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 lift

Missing test coverage for the Stripe-transfer/queueing/retry paths.

Only pure calculation helpers are tested. executeTaskPayout, queueTaskPayoutForVerifiedClaim, reconcileQueuedPayoutReadiness, and retryDueTaskPayouts — the functions that actually move money and are most exposed to the concurrency issue flagged in task-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 win

Extract 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 win

Live-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 matches sk_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 win

Add a page title via getRouteMetadata().

This new page has no metadata/generateMetadata export, leaving the browser tab title unset. As per coding guidelines, packages/web/src/app/**/*.{ts,tsx} should "Use getRouteMetadata() from routes.ts for page titles instead of hardcoding them," with routes.ts as the source of truth for titles/descriptions. Verify routes.ts has (or add) an entry for this admin route and wire up export 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 value

Auth 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 win

Refund/dispute handlers aren't scoped by order_type like the other task-funding branches.

charge.refunded and charge.dispute.created unconditionally call recordTaskFundingChargeRefunded/recordTaskFundingChargeDisputed for every charge, relying on taskFundingPayment.findFirst returning null for unrelated charges (shirts, store offers, generic donations). This works today but is inconsistent with the explicit order_type === TASK_FUNDING_ORDER_TYPE checks 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_type should 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 win

No 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 to create().

♻️ 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 win

Only the happy-path task-funding case is tested.

New handlers were added for checkout.session.async_payment_failed, checkout.session.expired, charge.refunded, and charge.dispute.created, but only checkout.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 (FAILED vs CANCELED, 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.tsx renders, so the fix belongs there (and this .logged-out.md file should then be regenerated with pnpm --filter @optimitron/web copy: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.md snapshot files; regenerate them with pnpm --filter @optimitron/web copy: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 win

Replace 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 win

Use 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

📥 Commits

Reviewing files that changed from the base of the PR and between 032b53b and 3bcebc3.

📒 Files selected for processing (31)
  • docs/LOCAL_DB.md
  • packages/db/prisma/migrations/20260701120000_add_task_funding_payments_and_payouts/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/package.json
  • packages/web/scripts/guard-local-dev-db.mjs
  • packages/web/src/app/admin/task-payouts/page.tsx
  • packages/web/src/app/api/cron/task-payouts/route.ts
  • packages/web/src/app/api/stripe/connect/account/route.ts
  • packages/web/src/app/api/stripe/connect/onboarding-link/route.ts
  • packages/web/src/app/api/stripe/connect/status/route.ts
  • packages/web/src/app/api/stripe/webhook/route.test.ts
  • packages/web/src/app/api/stripe/webhook/route.ts
  • packages/web/src/app/api/tasks/[id]/fund/checkout/route.ts
  • packages/web/src/app/api/tasks/[id]/funding-status/route.ts
  • packages/web/src/app/api/tasks/[id]/pledge/route.ts
  • packages/web/src/app/fund/page.logged-out.md
  • packages/web/src/app/fund/page.tsx
  • packages/web/src/app/tasks/[id]/page.tsx
  • packages/web/src/components/task-funding/TaskFundingCheckoutForm.tsx
  • packages/web/src/components/task-funding/TaskFundingProgress.tsx
  • packages/web/src/components/task-funding/__tests__/TaskFundingProgress.test.tsx
  • packages/web/src/components/tasks/StripeConnectStatusPanel.tsx
  • packages/web/src/lib/__tests__/task-payouts.server.test.ts
  • packages/web/src/lib/routes.ts
  • packages/web/src/lib/stripe-connect.server.ts
  • packages/web/src/lib/task-funding/__tests__/status.server.test.ts
  • packages/web/src/lib/task-funding/payments.server.ts
  • packages/web/src/lib/task-funding/status.server.ts
  • packages/web/src/lib/task-payouts.server.ts
  • packages/web/src/lib/tasks.server.ts
  • packages/web/vercel.json

Comment thread packages/db/prisma/schema.prisma
Comment thread packages/db/prisma/schema.prisma
Comment thread packages/db/prisma/schema.prisma
Comment thread packages/web/scripts/guard-local-dev-db.mjs
Comment thread packages/web/src/lib/task-funding/payments.server.ts
Comment thread packages/web/src/lib/task-payouts.server.ts
Comment thread packages/web/src/lib/task-payouts.server.ts
Comment thread packages/web/src/lib/task-payouts.server.ts
Comment thread packages/web/src/lib/tasks.server.ts
mikepsinn and others added 2 commits July 1, 2026 21:39
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>
mikepsinn and others added 4 commits July 1, 2026 23:34
#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>
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