← back to Ventura Claw Leads
scripts/stripe-bootstrap-v01.js
163 lines
#!/usr/bin/env node
// VCL v0.2 #4 bootstrap — provisions 3 subscription products (VCL Starter,
// VCL Standard, VCL Premier) + 1 webhook endpoint pointing at
// https://leads.venturaclaw.com/webhooks/stripe in the same Stripe LIVE account
// that already powers NPH + lawyer + 3 home-history sites. Idempotent: reuses
// products/prices keyed by metadata.vcl_tier; reuses webhook keyed by URL.
//
// CRITICAL: every product, price, and webhook is tagged with
// metadata.project='ventura-claw-leads' so the per-project webhook handler
// can ignore events that don't belong to it (since multiple projects share
// the same Stripe account, ALL endpoints receive ALL events for matching types).
//
// Output: writes summary JSON to /tmp/vcl-stripe-bootstrap.json with the IDs
// to fan via /secrets. Webhook secret is written one-time to a chmod-600
// file at /tmp/.vcl-stripe-webhook-secret; route via /secrets and rm.
const fs = require('node:fs');
const path = require('node:path');
require('dotenv').config({ path: path.resolve(__dirname, '..', '.env') });
const Stripe = require('stripe');
const SK = process.env.STRIPE_SECRET_KEY;
if (!SK || !SK.startsWith('sk_live_')) {
console.error('refusing to run: STRIPE_SECRET_KEY missing or not a live key');
process.exit(2);
}
const stripe = new Stripe(SK, { apiVersion: '2024-10-28.acacia' });
const PUBLIC_URL = (process.env.VCL_PUBLIC_URL || 'https://leads.venturaclaw.com').replace(/\/+$/, '');
const WEBHOOK_URL = `${PUBLIC_URL}/webhooks/stripe`;
const PLAN = [
{
tier: 'starter',
product_name: 'Ventura Claw — Starter',
description: 'Custom listing on Ventura Claw — claimed badge, priority over free listings, monthly traffic report.',
prices: [
{ env: 'STRIPE_PRICE_VCL_STARTER_MONTH', cents: 4900, interval: 'month' }
]
},
{
tier: 'standard',
product_name: 'Ventura Claw — Standard',
description: 'Featured-tier listing on Ventura Claw — appears above free listings, sidebar feature in category, lead-form fields per profile, sub-60s lead delivery.',
prices: [
{ env: 'STRIPE_PRICE_VCL_STANDARD_MONTH', cents: 9900, interval: 'month' }
]
},
{
tier: 'premier',
product_name: 'Ventura Claw — Premier',
description: 'Top-tier listing on Ventura Claw — home-page featured rotation, top placement in category search, priority lead-form, quarterly co-marketing.',
prices: [
{ env: 'STRIPE_PRICE_VCL_PREMIER_MONTH', cents: 19900, interval: 'month' }
]
}
];
// Subscription-only events. NO payment_intent / checkout.session events here —
// those belong to NPH (booking deposits) not VCL (subscription only).
const WEBHOOK_EVENTS = [
'customer.subscription.created',
'customer.subscription.updated',
'customer.subscription.deleted',
'invoice.payment_succeeded',
'invoice.payment_failed',
'customer.created',
'customer.updated',
'customer.deleted'
];
(async () => {
const out = { products: [], prices: {}, webhook: null };
// 1. Find or create products by metadata.vcl_tier
const existing = await stripe.products.list({ active: true, limit: 100 });
const byTier = {};
for (const p of existing.data) {
if (p.metadata && p.metadata.vcl_tier) byTier[p.metadata.vcl_tier] = p;
}
for (const planTier of PLAN) {
let product = byTier[planTier.tier];
if (product) {
console.log(`[products] reusing ${product.id} for vcl_tier=${planTier.tier}`);
} else {
product = await stripe.products.create({
name: planTier.product_name,
description: planTier.description,
metadata: { vcl_tier: planTier.tier, project: 'ventura-claw-leads' }
});
console.log(`[products] created ${product.id} for vcl_tier=${planTier.tier}`);
}
out.products.push({ tier: planTier.tier, id: product.id, name: product.name });
// 2. Find or create prices
const existingPrices = await stripe.prices.list({ product: product.id, active: true, limit: 100 });
for (const priceSpec of planTier.prices) {
const match = existingPrices.data.find(pr =>
pr.unit_amount === priceSpec.cents &&
pr.recurring?.interval === priceSpec.interval &&
pr.currency === 'usd'
);
let price;
if (match) {
price = match;
console.log(` [prices] reusing ${price.id} (${priceSpec.cents}¢/${priceSpec.interval}) for ${priceSpec.env}`);
} else {
price = await stripe.prices.create({
product: product.id,
currency: 'usd',
unit_amount: priceSpec.cents,
recurring: { interval: priceSpec.interval },
metadata: { vcl_tier: planTier.tier, env_var: priceSpec.env, project: 'ventura-claw-leads' }
});
console.log(` [prices] created ${price.id} (${priceSpec.cents}¢/${priceSpec.interval}) for ${priceSpec.env}`);
}
out.prices[priceSpec.env] = price.id;
}
}
// 3. Find or create webhook endpoint
const endpoints = await stripe.webhookEndpoints.list({ limit: 100 });
const existingWh = endpoints.data.find(e => e.url === WEBHOOK_URL);
let endpoint, secret;
if (existingWh) {
endpoint = existingWh;
console.log(`[webhook] reusing endpoint ${endpoint.id} → ${WEBHOOK_URL}`);
const missing = WEBHOOK_EVENTS.filter(e => !endpoint.enabled_events.includes(e));
if (missing.length) {
const merged = Array.from(new Set([...endpoint.enabled_events, ...WEBHOOK_EVENTS]));
endpoint = await stripe.webhookEndpoints.update(endpoint.id, { enabled_events: merged });
console.log(` [webhook] added ${missing.length} missing events`);
}
secret = null;
} else {
endpoint = await stripe.webhookEndpoints.create({
url: WEBHOOK_URL,
enabled_events: WEBHOOK_EVENTS,
metadata: { project: 'ventura-claw-leads' }
});
secret = endpoint.secret; // ONE-TIME
console.log(`[webhook] created endpoint ${endpoint.id} → ${WEBHOOK_URL}`);
}
out.webhook = {
id: endpoint.id, url: endpoint.url,
events: endpoint.enabled_events.length,
secret_revealed: !!secret
};
const summary = path.resolve('/tmp/vcl-stripe-bootstrap.json');
fs.writeFileSync(summary, JSON.stringify(out, null, 2));
console.log(`\n[summary] wrote ${summary}`);
if (secret) {
const secretPath = '/tmp/.vcl-stripe-webhook-secret';
fs.writeFileSync(secretPath, secret, { mode: 0o600 });
fs.chmodSync(secretPath, 0o600);
console.log(`[summary] webhook secret → ${secretPath} (chmod 600) — route via /secrets and rm`);
} else {
console.log('[summary] webhook secret NOT revealed (endpoint already existed). If lost, rotate via stripe.webhookEndpoints.update.');
}
})().catch(err => { console.error('FATAL:', err.message); process.exit(1); });