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
31 changes: 25 additions & 6 deletions api/admin/create-checkout-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ module.exports = async function handler(req, res) {

const body = req.body || {};
const claimId = typeof body.claimId === 'string' ? body.claimId.trim() : '';
const forceNew = body.forceNew === true;
if (!claimId) return res.status(400).json({ ok: false, status: 'INVALID_CLAIM_ID' });

let stripe;
Expand Down Expand Up @@ -83,7 +84,7 @@ module.exports = async function handler(req, res) {
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) {
if (claim.status === 'payment_pending' && claim.stripe_checkout_session_id && !forceNew) {
return res.status(200).json({
ok: true,
status: 'CHECKOUT_SESSION_CREATED',
Expand Down Expand Up @@ -146,8 +147,13 @@ module.exports = async function handler(req, res) {
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()`,
on conflict (claim_id, provider)
do update set stripe_checkout_session_id = excluded.stripe_checkout_session_id,
amount_cents = excluded.amount_cents,
currency = excluded.currency,
status = excluded.status,
metadata_json = excluded.metadata_json,
updated_at = now()`,
[claimId, session.id, 2000, JSON.stringify({ checkoutUrl: session.url || null })]
);

Expand All @@ -163,10 +169,17 @@ module.exports = async function handler(req, res) {
[claimId, 2000, session.id]
);

const eventType = forceNew && fromStatus === 'payment_pending'
? 'payment.checkout_regenerated'
: 'payment.checkout_created';
const eventMessage = forceNew && fromStatus === 'payment_pending'
? 'Stripe checkout regenerated.'
: 'Stripe checkout created.';

await db.query(
`insert into claim_events (claim_id, event_type, actor, message, event_json)
values ($1, 'payment.checkout_created', 'system', $2, $3::jsonb)`,
[claimId, 'Stripe checkout created.', JSON.stringify({ sessionId: session.id, checkoutUrl: session.url || null })]
values ($1, $2, 'system', $3, $4::jsonb)`,
[claimId, eventType, eventMessage, JSON.stringify({ sessionId: session.id, checkoutUrl: session.url || null })]
);

if (fromStatus === 'cards_published') {
Expand All @@ -177,7 +190,13 @@ 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 });
return res.status(200).json({
ok: true,
status: forceNew && fromStatus === 'payment_pending' ? 'CHECKOUT_SESSION_REGENERATED' : 'CHECKOUT_SESSION_CREATED',
claimId,
checkoutUrl: session.url || null,
sessionId: session.id
});
} catch (error) {
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.' });
Expand Down
4 changes: 2 additions & 2 deletions public/admin/claims.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ <h1>CommandLayer Claims Admin</h1><p class="muted">Internal operator dashboard f
async function loadDetail(id){s.selected=id;renderClaims();const r=await fetch(`/api/admin/claim?claimId=${encodeURIComponent(id)}`,{headers:{Authorization:headers().Authorization}});const d=await r.json();if(!r.ok||!d.ok)return; s.detail=d; renderDetail();}
async function action(action,p={}){const r=await fetch('/api/admin/claim-action',{method:'POST',headers:headers(),body:JSON.stringify({claimId:s.selected,action,actor:'admin',...p})});const d=await r.json();if(!r.ok||!d.ok){s.error=`${d.status}: ${d.error}`;renderDetail();return;}s.error=null;await loadClaims();await loadDetail(s.selected);}
async function publish(){const r=await fetch('/api/admin/publish-agent-cards',{method:'POST',headers:headers(),body:JSON.stringify({claimId:s.selected})});const d=await r.json();if(!r.ok||!d.ok){s.error=`${d.status}: ${d.error}`;renderDetail();return;}s.error=null;await loadClaims();await loadDetail(s.selected);}
async function createCheckoutSession(claimId){if(!claimId){s.error='400 — claimId is required';renderDetail();return;}s.error=null;s.checkoutUrl=null;s.checkoutLoading=true;renderDetail();try{const r=await fetch('/api/admin/create-checkout-session',{method:'POST',headers:headers(),body:JSON.stringify({claimId})});const d=await r.json().catch(()=>({}));if(!r.ok||!d.ok){const detail=d?.debug?.message?`\nDetails: ${d.debug.message}`:'';s.error=`Checkout failed:\n${d.status||r.status} — ${d.error||'Request failed'}${detail}`;return;}s.checkoutUrl=d.checkoutUrl||d.url||null;await loadClaims();await loadDetail(claimId);}catch(e){s.error=`500 — ${e?.message||'Request failed'}`;}finally{s.checkoutLoading=false;renderDetail();}}
async function createCheckoutSession(claimId,forceNew=false){if(!claimId){s.error='400 — claimId is required';renderDetail();return;}s.error=null;s.checkoutUrl=null;s.checkoutLoading=true;renderDetail();try{const r=await fetch('/api/admin/create-checkout-session',{method:'POST',headers:headers(),body:JSON.stringify({claimId,forceNew})});const d=await r.json().catch(()=>({}));if(!r.ok||!d.ok){const detail=d?.debug?.message?`\nDetails: ${d.debug.message}`:'';s.error=`Checkout failed:\n${d.status||r.status} — ${d.error||'Request failed'}${detail}`;return;}s.checkoutUrl=d.checkoutUrl||d.url||null;await loadClaims();await loadDetail(claimId);}catch(e){s.error=`500 — ${e?.message||'Request failed'}`;}finally{s.checkoutLoading=false;renderDetail();}}
function checkoutUrlFromClaim(claim){return s.checkoutUrl || claim?.stripe_checkout_url || claim?.payment_checkout_url || null;}
function openCheckout(claim){const url=checkoutUrlFromClaim(claim);if(url)window.open(url,'_blank');}
function copyCheckout(claim){const url=checkoutUrlFromClaim(claim);if(url)navigator.clipboard.writeText(url);}
Expand All @@ -72,7 +72,7 @@ <h1>CommandLayer Claims Admin</h1><p class="muted">Internal operator dashboard f
if(claim.status==='created'){mk('Approve',()=>action('approve',{notes:document.getElementById('notesInput').value}),'btn btn-primary');mk('Reject',()=>action('reject',{reason:document.getElementById('reasonInput').value}),'btn btn-secondary');mk('Mark failed',()=>action('mark_failed',{reason:document.getElementById('reasonInput').value}),'btn btn-danger');}
if(claim.status==='approved'){mk('Publish agent cards',()=>publish(),'btn btn-primary');mk('Mark failed',()=>action('mark_failed',{reason:document.getElementById('reasonInput').value}),'btn btn-danger');mk('Add note',()=>action('add_note',{notes:document.getElementById('notesInput').value,reason:document.getElementById('reasonInput').value}),'btn');}
if(claim.status==='cards_published'){if(missing)mk('Repair / Publish cards',()=>publish(),'btn btn-primary');mk(s.checkoutLoading?'Creating checkout...':'Create $20 checkout',()=>createCheckoutSession(claim.claim_id),'btn btn-primary',{disabled:s.checkoutLoading,id:'createCheckoutBtn'});mk('Add note',()=>action('add_note',{notes:document.getElementById('notesInput').value,reason:document.getElementById('reasonInput').value}),'btn');}
if(claim.status==='payment_pending'){mk('Open checkout',()=>openCheckout(claim),'btn btn-secondary');mk('Copy checkout URL',()=>copyCheckout(claim),'btn btn-secondary');mk('Add note',()=>action('add_note',{notes:document.getElementById('notesInput').value,reason:document.getElementById('reasonInput').value}),'btn');}
if(claim.status==='payment_pending'){mk('Open checkout',()=>openCheckout(claim),'btn btn-secondary');mk('Copy checkout URL',()=>copyCheckout(claim),'btn btn-secondary');mk(s.checkoutLoading?'Creating checkout...':'Regenerate checkout session',()=>createCheckoutSession(claim.claim_id,true),'btn btn-primary',{disabled:s.checkoutLoading,id:'regenerateCheckoutBtn'});mk('Add note',()=>action('add_note',{notes:document.getElementById('notesInput').value,reason:document.getElementById('reasonInput').value}),'btn');}
if(claim.status==='failed'||claim.status==='rejected'){mk('Add note',()=>action('add_note',{notes:document.getElementById('notesInput').value,reason:document.getElementById('reasonInput').value}),'btn');}
const copy=document.getElementById('copyClaim');if(copy)copy.onclick=()=>navigator.clipboard.writeText(claim.claim_id);const cu=document.getElementById('copyUrls');if(cu)cu.onclick=()=>navigator.clipboard.writeText(urls.join('\n'));const of=document.getElementById('openFirst');if(of)of.onclick=()=>window.open(urls[0],'_blank');const ccu=document.getElementById('copyCheckoutUrl');if(ccu)ccu.onclick=()=>navigator.clipboard.writeText(ccu.dataset.url||'');document.querySelectorAll('.copy-url').forEach((btn)=>{btn.onclick=()=>navigator.clipboard.writeText(btn.dataset.url||'');}); }
const status=(m)=>document.getElementById('status').textContent=m;
Expand Down
9 changes: 8 additions & 1 deletion tests/api-payments.test.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading