diff --git a/api/admin/create-checkout-session.js b/api/admin/create-checkout-session.js index 06cd9a1..032b176 100644 --- a/api/admin/create-checkout-session.js +++ b/api/admin/create-checkout-session.js @@ -1,9 +1,17 @@ 'use strict'; -const Stripe = require('../../lib/stripe-client'); +const createStripeClient = require('../../lib/stripe-client'); const db = require('../../lib/db'); const { requireAdminAuth } = require('./_auth'); +function asServiceUnavailable(res, status, error) { + return res.status(503).json({ ok: false, status, error }); +} + +function asConflict(res, status, error) { + return res.status(409).json({ ok: false, status, error }); +} + module.exports = async function handler(req, res) { res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Cache-Control', 'no-store'); @@ -14,25 +22,35 @@ module.exports = async function handler(req, res) { } if (!requireAdminAuth(req, res)) return; - if (!process.env.STRIPE_SECRET_KEY) { - return res.status(503).json({ ok: false, status: 'STRIPE_NOT_CONFIGURED' }); - } - const body = req.body || {}; const claimId = typeof body.claimId === 'string' ? body.claimId.trim() : ''; if (!claimId) return res.status(400).json({ ok: false, status: 'INVALID_CLAIM_ID' }); + let stripe; + try { + stripe = createStripeClient(process.env.STRIPE_SECRET_KEY); + } catch (error) { + if (error?.code === 'STRIPE_NOT_CONFIGURED') { + return asServiceUnavailable(res, 'STRIPE_NOT_CONFIGURED', 'Stripe secret key is not configured.'); + } + if (error?.code === 'STRIPE_SECRET_KEY_INVALID') { + return asServiceUnavailable(res, 'STRIPE_SECRET_KEY_INVALID', error.message); + } + console.error('ADMIN_CREATE_CHECKOUT_STRIPE_INIT_FAILED', { message: error?.message, code: error?.code, claimId }); + return asServiceUnavailable(res, 'STRIPE_NOT_CONFIGURED', 'Stripe secret key is not configured.'); + } + try { const claims = db.normalizeRows(await db.query('select * from claim_requests where claim_id = $1 limit 1', [claimId])); - if (!claims.length) return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND' }); + if (!claims.length) return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND', error: 'Claim not found.' }); const claim = claims[0]; if (claim.status === 'paid' || claim.payment_status === 'paid') { - return res.status(409).json({ ok: false, status: 'PAYMENT_ALREADY_COMPLETED' }); + return asConflict(res, 'PAYMENT_ALREADY_COMPLETED', 'Payment is already completed for this claim.'); } if (!['cards_published', 'payment_pending'].includes(claim.status)) { - return res.status(409).json({ ok: false, status: 'CLAIM_NOT_READY_FOR_PAYMENT' }); + return asConflict(res, 'CLAIM_NOT_READY_FOR_PAYMENT', 'Claim must be cards_published before creating checkout.'); } if (claim.status === 'payment_pending' && claim.stripe_checkout_session_id) { @@ -45,38 +63,53 @@ module.exports = async function handler(req, res) { }); } - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - const priceCents = Number.parseInt(process.env.STRIPE_FOUNDING_PRICE_CENTS || '2000', 10) || 2000; const siteUrl = process.env.COMMANDLAYER_SITE_URL || 'https://www.commandlayer.org'; const successUrl = `${siteUrl}/claim/status.html?claimId=${encodeURIComponent(claimId)}&payment=success`; const cancelUrl = `${siteUrl}/claim/status.html?claimId=${encodeURIComponent(claimId)}&payment=cancelled`; - const session = await stripe.checkout.sessions.create({ - mode: 'payment', - success_url: successUrl, - cancel_url: cancelUrl, - line_items: [{ - quantity: 1, - price_data: { - currency: 'usd', - unit_amount: priceCents, - product_data: { name: 'Founding Activation' } + let session; + try { + session = await stripe.checkout.sessions.create({ + mode: 'payment', + success_url: successUrl, + cancel_url: cancelUrl, + line_items: [{ + quantity: 1, + price_data: { + currency: 'usd', + unit_amount: 2000, + product_data: { + name: 'CommandLayer Founding Activation', + description: '10 Trust Verification agent namespaces' + } + } + }], + metadata: { + claimId, + tenant: claim.tenant || '', + packId: claim.pack_id || '', + product: 'founding_activation' } - }], - metadata: { - claimId, - tenant: claim.tenant || '', - packId: claim.pack_id || '', - product: 'founding_activation' + }); + } catch (error) { + console.error('ADMIN_CREATE_CHECKOUT_SESSION_FAILED', { message: error?.message, code: error?.code, claimId }); + const payload = { + ok: false, + status: 'CHECKOUT_SESSION_CREATE_FAILED', + error: 'Unable to create Stripe checkout session.' + }; + if (process.env.NODE_ENV !== 'production') { + payload.debug = { message: error?.message || 'Unknown Stripe error', code: error?.code || null }; } - }); + return res.status(502).json(payload); + } await db.query( `insert into claim_payments (claim_id, provider, stripe_checkout_session_id, amount_cents, currency, status, metadata_json) values ($1, 'stripe', $2, $3, 'usd', 'pending', $4::jsonb) on conflict (stripe_checkout_session_id) do update set status = excluded.status, metadata_json = excluded.metadata_json, updated_at = now()`, - [claimId, session.id, priceCents, JSON.stringify({ checkoutUrl: session.url || null })] + [claimId, session.id, 2000, JSON.stringify({ checkoutUrl: session.url || null })] ); const fromStatus = claim.status; @@ -88,7 +121,7 @@ module.exports = async function handler(req, res) { payment_currency = 'usd', stripe_checkout_session_id = $3 where claim_id = $1`, - [claimId, priceCents, session.id] + [claimId, 2000, session.id] ); await db.query( @@ -107,7 +140,7 @@ module.exports = async function handler(req, res) { return res.status(200).json({ ok: true, status: 'CHECKOUT_SESSION_CREATED', claimId, checkoutUrl: session.url || null, sessionId: session.id }); } catch (error) { - console.error('ADMIN_CREATE_CHECKOUT_SESSION_FAILED', { message: error.message, code: error.code }); + console.error('ADMIN_CREATE_CHECKOUT_SESSION_UNEXPECTED', { message: error?.message, code: error?.code, claimId }); return res.status(500).json({ ok: false, status: 'ADMIN_CREATE_CHECKOUT_SESSION_FAILED', error: 'Failed to create checkout session.' }); } }; diff --git a/lib/stripe-client.js b/lib/stripe-client.js index 4aa4e56..163e1a2 100644 --- a/lib/stripe-client.js +++ b/lib/stripe-client.js @@ -1,2 +1,20 @@ 'use strict'; -module.exports = require('stripe'); + +const Stripe = require('stripe'); + +function createStripeError(code, message) { + const error = new Error(message); + error.code = code; + return error; +} + +module.exports = function createStripeClient(secretKey, options = {}) { + const key = typeof secretKey === 'string' ? secretKey.trim() : ''; + if (!key) { + throw createStripeError('STRIPE_NOT_CONFIGURED', 'Stripe secret key is not configured.'); + } + if (key.startsWith('pk_')) { + throw createStripeError('STRIPE_SECRET_KEY_INVALID', 'Stripe secret key must be a server secret key (sk_*), not a publishable key.'); + } + return new Stripe(key, options); +}; diff --git a/public/admin/claims.html b/public/admin/claims.html index 074d1a6..c106dd2 100644 --- a/public/admin/claims.html +++ b/public/admin/claims.html @@ -8,14 +8,14 @@

CommandLayer Claims Admin

Internal operator dashboard for claim review and activation pipeline.

-
-

Claims

ClaimTenantPackStatusAgentsCreated

Select a claim to review.

+
+

Claims

Select a claim to review.