Skip to content

feat: add invite system with payment-gated reward and abuse guards - #36

Open
abhishakenp wants to merge 1 commit into
LinkForty:mainfrom
abhishakenp:feat/invite-system
Open

feat: add invite system with payment-gated reward and abuse guards#36
abhishakenp wants to merge 1 commit into
LinkForty:mainfrom
abhishakenp:feat/invite-system

Conversation

@abhishakenp

Copy link
Copy Markdown

Summary

Adds a referral invite system on top of LinkForty's existing deferred deep linking infrastructure. Designed for invite-based growth where the reward is gated on the invitee making a payment (not just signing up), making fake-account farming economically irrational.

Three abuse-prevention guards (at the payment event)

  1. Self-invite forbiddeninviterId === inviteeUserId → reject (deterministic, unbeatable)
  2. Already-on-platform rejectedinviteeCreatedAt < invite.createdAt → reject (invite is for NEW users only)
  3. Single consumptionWHERE status = 'pending' in the UPDATE prevents double-reward even under concurrent requests (DB-level atomicity)

New endpoints

Method Path Purpose
POST /api/invites Create a pending invite
POST /api/invites/:id/consume Consume on payment (runs all guards, fires webhook)
POST /api/invites/:id/expire Manually expire (no reward)
GET /api/invites/:inviterId List invites for an inviter
GET /api/invites/id/:id Get single invite by ID

On successful consumption, an invite_consumed_event webhook fires to the inviter's registered webhook endpoints.

Schema

New invites table with status (pendingconsumed | expired), inviter_id, invitee_user_id, payment_amount, reward_issued, and metadata JSONB. Migration is idempotent (CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS), consistent with the existing migration pattern in database.ts.

Design decisions (per user requirements)

  • Reusable links: no click cap — a user can click 5 times before deciding. The system only acts at the payment event.
  • Payment gate, not signup: the reward only fires when paymentAmount > 0 is provided. Signup alone triggers nothing.
  • Expiry on consume: once the invitee pays, the invite transitions to consumed and all future attempts are rejected. No separate device-dedup logic needed.
  • No max-clicks: explicitly not a feature.

Deductive proof

docs/invite-system-proof.md contains a formal proof of correctness for each invariant, with code citations and test evidence mapping.

Test plan

  • 22 unit tests in src/lib/invite.test.ts — every guard function, boundary cases, guard ordering, race conditions
  • 15 integration tests in src/routes/invites.test.ts — full HTTP flow, webhook firing, lifecycle
  • Full suite: 176 tests passing (37 new + 139 pre-existing, zero regressions)
  • tsc --noEmit typecheck clean
  • Zod validation returns 400 (not 500) for invalid input
Test Files  11 passed (11)
     Tests  176 passed (176)
  Duration  735ms

Generated with Devin

Adds a referral invite system on top of LinkForty's existing deferred deep
linking infrastructure. The system is designed for invite-based growth
where the reward is gated on the invitee making a payment (not just
signing up), making fake-account farming economically irrational.

Three guards enforce the abuse-prevention policy at the payment event:
1. Self-invite: inviterId === inviteeUserId → reject (deterministic)
2. Already-on-platform: inviteeCreatedAt < invite.createdAt → reject
3. Single consumption: WHERE status='pending' in UPDATE prevents
   double-reward even under concurrent requests (DB-level atomicity)

New endpoints:
- POST /api/invites — create a pending invite
- POST /api/invites/:id/consume — consume on payment (runs all guards)
- POST /api/invites/:id/expire — manually expire (no reward)
- GET /api/invites/:inviterId — list invites for an inviter
- GET /api/invites/id/:id — get single invite

On successful consumption, an `invite_consumed_event` webhook fires to
the inviter's registered webhook endpoints, carrying the payment amount,
reward amount, and both user identities.

37 new tests (22 unit + 15 integration) cover every guard, guard ordering,
race conditions, boundary cases, and the full create→consume→verify
lifecycle. Full suite: 176 tests passing.

Deductive proof of correctness in docs/invite-system-proof.md.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

3 participants