Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 63 additions & 30 deletions api/admin/create-checkout-session.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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(
Expand All @@ -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.' });
}
};
20 changes: 19 additions & 1 deletion lib/stripe-client.js
Original file line number Diff line number Diff line change
@@ -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);
};
Loading
Loading