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:
@@ -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.
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user