NexaPay purge: VYNDR is Stripe-only — remove all NexaPay traces

NexaPay was cross-project contamination (from another venture) — never a real
VYNDR payment path. Purged; Stripe path untouched.

Removed:
- web/src/services/nexapay.ts (createPaymentLink/getTransaction/HMAC verify)
- web/src/app/api/webhook/nexapay/route.ts (the only importer; Next-registered,
  reachable — now gone)
- NexaPay comments in email.ts + checkout/route.ts
- Active NexaPay entries in docs/SYSTEM-MANIFEST.md (route list, NEXAPAY_* env
  table, service row) + stale claim in wiring-data-train.md
- sw.js precache entry for the deleted webhook chunk

Verified: ZERO NexaPay in code (web/src, src, tests). Full suite 3833 green
(count unchanged — nothing depended on it, confirming it was dead). Web build
exit 0. sw.js parses clean. Stripe checkout untouched (Next→Express→Stripe).

FLAGGED FOR KEV (a repo delete cannot close these):
- Coolify env: remove NEXAPAY_API_KEY / NEXAPAY_WEBHOOK_SECRET / NEXAPAY_API_URL
- Revoke the NexaPay API key + webhook secret at NexaPay's dashboard; de-register
  the webhook if an account was ever configured
- DB column user_profiles.nexapay_customer_id is orphaned (no reader/writer) —
  drop via a follow-up migration (migration 011 left as history)

Cross-project check: ZERO Noctem-Supabase refs; VYNDR references only its own
Supabase (zmdnczhtdxcddsxzttub). NexaPay was the sole contamination found.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1
This commit is contained in:
Kev
2026-07-27 16:51:59 -04:00
parent 3592aba8d5
commit afb56b144b
9 changed files with 17 additions and 262 deletions
+1 -2
View File
@@ -9,8 +9,7 @@ const VALID_TIERS = new Set(['analyst', 'desk']);
/**
* Checkout proxy — Next.js → Express → Stripe.
*
* Session 8 cutover: previously this route created NexaPay payment
* links; now it forwards to the Express `/api/stripe/checkout` route
* This route forwards to the Express `/api/stripe/checkout` route
* (Session 3.4 + 7i) which creates a Stripe Checkout Session
* server-side. The browser never sees `sk_test_*` / `sk_live_*` —
* only the resulting `https://checkout.stripe.com/...` redirect URL.
-100
View File
@@ -1,100 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServiceRoleSupabase } from '@/lib/supabase';
import { verifyWebhookSignature, type NexaPayWebhookEvent, type NexaPayTier } from '@/services/nexapay';
import { sendPaymentReceipt } from '@/services/email';
export const dynamic = 'force-dynamic';
// We need the raw body to verify the HMAC signature.
export const runtime = 'nodejs';
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get('x-nexapay-signature');
if (!verifyWebhookSignature(rawBody, signature)) {
return NextResponse.json({ error: 'invalid signature' }, { status: 401 });
}
let event: NexaPayWebhookEvent;
try {
event = JSON.parse(rawBody) as NexaPayWebhookEvent;
} catch {
return NextResponse.json({ error: 'invalid body' }, { status: 400 });
}
const sb = getServiceRoleSupabase();
if (!sb) {
console.error('[nexapay webhook] Supabase service role not configured');
return NextResponse.json({ error: 'misconfigured' }, { status: 500 });
}
const userId = event.data.metadata?.userId;
const tier = event.data.metadata?.tier as NexaPayTier | undefined;
const founderPricing = event.data.metadata?.founderPricing === 'true';
if (!userId || !tier) {
return NextResponse.json({ ok: true, ignored: 'missing metadata' });
}
switch (event.type) {
case 'payment.succeeded': {
const subscription_end = new Date();
subscription_end.setUTCDate(subscription_end.getUTCDate() + 30);
const { error } = await sb
.from('user_profiles')
.update({
tier,
subscription_status: 'active',
subscription_start: new Date().toISOString(),
subscription_end: subscription_end.toISOString(),
cancel_at_period_end: false,
founder_pricing: founderPricing,
nexapay_customer_id: event.data.customer_id ?? null,
})
.eq('id', userId);
if (error) {
console.error('[nexapay webhook] update failed', error);
return NextResponse.json({ error: 'update_failed' }, { status: 500 });
}
// Fire-and-forget receipt email. Don't block the webhook ACK.
const { data: profileRow } = await sb
.from('user_profiles')
.select('email')
.eq('id', userId)
.maybeSingle();
if (profileRow?.email) {
void sendPaymentReceipt(profileRow.email, {
tier,
amount: `$${(event.data.amount / 100).toFixed(2)}`,
renewsAt: subscription_end.toISOString().slice(0, 10),
});
}
break;
}
case 'payment.failed': {
await sb
.from('user_profiles')
.update({ subscription_status: 'grace_period' })
.eq('id', userId);
break;
}
case 'payment.refunded':
case 'subscription.canceled': {
await sb
.from('user_profiles')
.update({
subscription_status: 'canceled',
cancel_at_period_end: true,
})
.eq('id', userId);
break;
}
}
return NextResponse.json({ ok: true });
}