Repository navigation
Pricing from one source, CoinPay webhook settlement fixes, free post cap, fee disclosure - #596
Merged
Merged
Conversation
ThreatCrush Security Scan46 finding(s) HIGH/CRITICAL: 1 | MEDIUM: 7 | LOW: 38
Snippets are redacted; ThreatCrush never prints matched credential material. |
… enforce the free post cap - src/lib/plans.ts is the one source for prices ($9/mo, $90/yr, $100 lifetime), perks, FREE_MONTHLY_GIG_POSTS and isPaidPlan/hasPaidAccess. - Lifetime equals Pro: marketplace seller fees (skills, MCP, prompts), gig post cap, Stripe checkout guard. - Free 10 posts/month cap actually counts: usage row is created with ignoreDuplicates instead of being reset to 1 on every post, and the cap is checked on POST /api/gigs, PATCH /status and PUT when a gig becomes active. Migration makes increment_gig_usage atomic and drops the user write policies. - CoinPay webhook: funding payments of $50+ grant Lifetime and write funding_rewards_log (idempotent); forwarded without confirmed activates once; metadata is merged; the plan is stored at create time and read back (annual +12 months, lifetime no end); payment.failed maps to failed; late expired/failed never un-pay a settled row. - Status poll writes through the service client, maps provider statuses and settles through the same code as the webhook. - New crons: /api/cron/expire-payments (pending past expires_at or 24h) and /api/cron/expire-subscriptions (CoinPay Pro 3+ days past end -> free; never Lifetime or Stripe-managed). - CLI `ugig payments status` calls the real status route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re payment - Landing, for-employers, for-candidates, dashboard, gigs/new, subscription page, funding page, docs, openapi and openaccess read src/lib/plans.ts: Pro $9/month (card or crypto), $90/year and Lifetime $100 crypto only. - Perks are only the real ones: unlimited gig posts and the 2% marketplace seller fee. Removed featured badge/placement, priority support, advanced analytics, "Post unlimited gigs for free", funding badges, premium placement and API access. The only funding perk is $50+ = Lifetime. - Subscription page no longer imports the Stripe SDK into the client; adds crypto monthly/annual checkout; crypto Pro shows "Paid through" and no Stripe-only buttons; Lifetime cannot "downgrade". - Fee disclosure before payment (Terms 5): escrow 5% before the escrow is created, zap 2% split, seller fee and net on listing forms, exact buyer price on purchase buttons. Rates come from src/lib/constants.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stripe is no longer a payment rail. Pro ($9/mo, $90/yr) and Lifetime ($100) are sold only through CoinPay. - Subscription page: the card button is gone; monthly, annual and lifetime all go through the CoinPay checkout. Shows a confirmation when CoinPay returns the buyer (?payment=success, now kept by /settings/billing). - POST /api/subscriptions/checkout returns 410 and points at the CoinPay create route (prod has no Stripe-billed subscriptions). The Stripe webhook, portal and cancel routes stay for a legacy subscriber. Removed the unused Stripe browser client and the createCheckout API helper. - Funding: the card option is removed from the UI and refused by /api/funding/create-invoice. - Copy (plans.ts rails, pricing block, landing, docs, openapi) no longer mentions cards or Stripe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ralyodio
force-pushed
the
fix/pricing-and-payment-webhooks
branch
from
October 6, 2026 10:46
70ac076 to
6a68ec1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements PRD 05 (payment reliability), PRD 06 (pricing and plans) and the fee-disclosure part of PRD 07, using the decisions already made.
Pricing (PRD 06)
src/lib/plans.tsis now the only place prices and perks are defined. It holds the prices (Pro $9/month, $90/year, Lifetime $100), plan names, perks,FREE_MONTHLY_GIG_POSTS = 10,isPaidPlanandhasPaidAccess. Every page, doc, the OpenAPI spec, openaccess.json and the CoinPay create route read from it.POST /api/subscriptions/checkoutnow returns 410 and points at/api/payments/coinpayportal/create. Prod has no Stripe-billed subscriptions (checked read-only).createCheckoutAPI helper, and the checkout/portal entries inopenapi.json./api/funding/create-invoicerefusescurrency: "card".docs/deployment.mdmarks Stripe as retired.?payment=success./settings/billingnow keeps that query string when it redirects.Free 10 posts/month cap (now actually enforced)
POST /api/gigsupsertedposts_count: 1and then incremented, so every free account sat at 2 and never hit the cap.ignoreDuplicates, and the RPC does the increment.POST /api/gigs,PATCH /api/gigs/[id]/status, andPUT /api/gigs/[id]when it changes the status. Hiring gigs and for-hire ads both count. Shared code is insrc/lib/gig-usage.ts.20261006131000_gig_usage_atomic_increment.sql:increment_gig_usagebecomes an atomic insert-or-increment.gig_usage. Until now any user could reset their own counter through PostgREST.CoinPay webhook and status (PRD 05)
Settlement code shared by the webhook and the status poll is in
src/lib/payments/checkout-settlement.ts.funding_rewards_logonce per funding payment. Anonymous contributions are recorded but grant nothing.forwardedwith noconfirmedbefore it now activates the purchase, exactly once. A guarded status transition makes sure of that. Metadata is merged, not replaced. Aconfirmedthat arrives afterforwardedno longer moves the status back.planon the local row.plan=lifetimewith no end date.payment.failednow marks the paymentfailed. A lateexpiredorfailedevent never un-pays a settled row (checkout or funding).CRON_SECRETauth and?dry_run=1:POST /api/cron/expire-payments: pendingpaymentsrows pastmetadata.expires_at(or 24h after creation) becomeexpired.POST /api/cron/expire-subscriptions: CoinPay Pro more than 3 days pastcurrent_period_enddrops to Free, with an in-app notification. It never touches Lifetime or Stripe-managed rows (stripe_subscription_idset).ugig payments status <id>now calls/api/payments/coinpayportal/status?payment_id=. The--planhelp lists lifetime.Fee disclosure before payment (Terms §5)
Rates come from
src/lib/constants.ts. I addedESCROW_FEE_RATEand fixed the comment that claimed a 2% withdrawal fee. The preview code (src/lib/fees.ts) does the same arithmetic as the server.Migration (not applied)
supabase/migrations/20261006131000_gig_usage_atomic_increment.sqlCron schedules to add
POST /api/cron/expire-payments: hourlyPOST /api/cron/expire-subscriptions: dailyData check (read-only)
No funding payment of $50+ exists on prod (all 20 are under $50), so there is nothing to backfill for bug 1.
Checks
The pre-commit hook is broken in fresh worktrees, so I ran these by hand on the committed tree:
npx tsc --noEmit: clean.--no-file-parallelism) on the final commit: 250 files, 2307 tests pass.src/test/fake-supabase.ts). They cover funding, forwarded-only, plan carry-over, failed/expired, the status poll, both crons, the post cap across POST/PATCH/PUT, checkout guards and the fee UI, the 410 checkout and the funding card refusal.🤖 Generated with Claude Code