Merge pull request 'refactor: delete superseded checkout routes and dedupe cart checkout' (#17) from refactor/remove-dead-checkout-routes into main
SonarQube Analysis / sonarqube (push) Successful in 5m12s
Tests / backend-unit (push) Successful in 57s
Tests / backend-integration (push) Successful in 1m47s
Tests / frontend-e2e (push) Failing after 1m31s

Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
2026-08-14 17:59:42 -05:00
3 changed files with 60 additions and 238 deletions
+60 -43
View File
@@ -22,9 +22,21 @@ async function getAccessToken(): Promise<string> {
return data.access_token;
}
interface CartItem {
id: number;
name: string;
price_cents: number;
}
interface LockedCart {
cartId: number;
items: CartItem[];
totalCents: number;
}
// Locks the customer's cart, verifies every item is still reserved to them,
// and returns { cartId, items: [{id, name, price_cents}], totalCents }.
async function loadLockedCart(client: any, customerId: number) {
async function loadLockedCart(client: any, customerId: number): Promise<LockedCart | null> {
const { rows: cartRows } = await client.query(`SELECT id FROM carts WHERE customer_id = $1`, [customerId]);
if (!cartRows.length) return null;
const cartId = cartRows[0].id;
@@ -37,10 +49,49 @@ async function loadLockedCart(client: any, customerId: number) {
[cartId]
);
if (!items.length) return { cartId, items: [], totalCents: 0 };
const totalCents = items.reduce((sum: number, it: any) => sum + it.price_cents, 0);
const totalCents = items.reduce((sum: number, it: CartItem) => sum + it.price_cents, 0);
return { cartId, items, totalCents };
}
type OpenedCheckout =
| { ok: true; checkoutId: number; cart: LockedCart }
| { ok: false; error: string };
// Both checkout flows open the same way: confirm the shipping address belongs
// to the caller, lock the cart, and record a pending checkout with its line
// items. The caller owns the transaction — on `ok: false` it should roll back
// and return the error as a 400.
async function openCheckout(
client: any,
customerId: number,
shippingAddressId: number,
processor: string,
processorOrderId: string | null
): Promise<OpenedCheckout> {
const { rows: addrRows } = await client.query(
`SELECT id FROM shipping_addresses WHERE id = $1 AND customer_id = $2`,
[shippingAddressId, customerId]
);
if (!addrRows.length) return { ok: false, error: 'invalid shipping address' };
const cart = await loadLockedCart(client, customerId);
if (!cart || !cart.items.length) return { ok: false, error: 'cart is empty' };
const { rows: checkoutRows } = await client.query(
`INSERT INTO checkouts (customer_id, shipping_address_id, processor, processor_order_id, amount_cents, status)
VALUES ($1, $2, $3, $4, $5, 'pending') RETURNING id`,
[customerId, shippingAddressId, processor, processorOrderId, cart.totalCents]
);
const checkoutId = checkoutRows[0].id;
for (const it of cart.items) {
await client.query(
`INSERT INTO checkout_items (checkout_id, item_id, price_cents) VALUES ($1, $2, $3)`,
[checkoutId, it.id, it.price_cents]
);
}
return { ok: true, checkoutId, cart };
}
router.post('/paypal/create', requireCustomer, async (req: Request, res: Response) => {
const { shippingAddressId } = req.body;
if (!shippingAddressId) return res.status(400).json({ error: 'shippingAddressId is required' });
@@ -48,27 +99,9 @@ router.post('/paypal/create', requireCustomer, async (req: Request, res: Respons
const client = await pool.connect();
try {
await client.query('BEGIN');
const { rows: addrRows } = await client.query(
`SELECT id FROM shipping_addresses WHERE id = $1 AND customer_id = $2`,
[shippingAddressId, req.customerId]
);
if (!addrRows.length) { await client.query('ROLLBACK'); return res.status(400).json({ error: 'invalid shipping address' }); }
const cart = await loadLockedCart(client, req.customerId as number);
if (!cart || !cart.items.length) { await client.query('ROLLBACK'); return res.status(400).json({ error: 'cart is empty' }); }
const { rows: checkoutRows } = await client.query(
`INSERT INTO checkouts (customer_id, shipping_address_id, processor, amount_cents, status)
VALUES ($1, $2, 'paypal', $3, 'pending') RETURNING id`,
[req.customerId, shippingAddressId, cart.totalCents]
);
const checkoutId = checkoutRows[0].id;
for (const it of cart.items) {
await client.query(
`INSERT INTO checkout_items (checkout_id, item_id, price_cents) VALUES ($1, $2, $3)`,
[checkoutId, it.id, it.price_cents]
);
}
const opened = await openCheckout(client, req.customerId as number, shippingAddressId, 'paypal', null);
if (!opened.ok) { await client.query('ROLLBACK'); return res.status(400).json({ error: opened.error }); }
const { checkoutId, cart } = opened;
const token = await getAccessToken();
const currency = process.env.SITE_CURRENCY || 'USD';
@@ -84,7 +117,7 @@ router.post('/paypal/create', requireCustomer, async (req: Request, res: Respons
value: (cart.totalCents / 100).toFixed(2),
breakdown: { item_total: { currency_code: currency, value: (cart.totalCents / 100).toFixed(2) } }
},
items: cart.items.map((it: any) => ({
items: cart.items.map((it: CartItem) => ({
name: it.name.slice(0, 127),
quantity: '1',
unit_amount: { currency_code: currency, value: (it.price_cents / 100).toFixed(2) }
@@ -168,26 +201,10 @@ router.post('/demo/purchase', requireCustomer, async (req: Request, res: Respons
const client = await pool.connect();
try {
await client.query('BEGIN');
const { rows: addrRows } = await client.query(
`SELECT id FROM shipping_addresses WHERE id = $1 AND customer_id = $2`,
[shippingAddressId, req.customerId]
);
if (!addrRows.length) { await client.query('ROLLBACK'); return res.status(400).json({ error: 'invalid shipping address' }); }
const opened = await openCheckout(client, req.customerId as number, shippingAddressId, 'demo', `demo-${Date.now()}`);
if (!opened.ok) { await client.query('ROLLBACK'); return res.status(400).json({ error: opened.error }); }
const cart = await loadLockedCart(client, req.customerId as number);
if (!cart || !cart.items.length) { await client.query('ROLLBACK'); return res.status(400).json({ error: 'cart is empty' }); }
const { rows: checkoutRows } = await client.query(
`INSERT INTO checkouts (customer_id, shipping_address_id, processor, processor_order_id, amount_cents, status)
VALUES ($1, $2, 'demo', $3, $4, 'pending') RETURNING id`,
[req.customerId, shippingAddressId, `demo-${Date.now()}`, cart.totalCents]
);
const checkoutId = checkoutRows[0].id;
for (const it of cart.items) {
await client.query(`INSERT INTO checkout_items (checkout_id, item_id, price_cents) VALUES ($1, $2, $3)`, [checkoutId, it.id, it.price_cents]);
}
await completeCheckout(client, checkoutId, 'demo', null, { demo: true });
await completeCheckout(client, opened.checkoutId, 'demo', null, { demo: true });
await client.query('COMMIT');
res.json({ status: 'completed' });
} catch (err) {
-39
View File
@@ -1,39 +0,0 @@
import { Router, Request, Response } from 'express';
import { pool } from '../db';
const router = Router();
router.post('/:id/purchase', async (req: Request, res: Response) => {
if (process.env.DEMO_MODE === 'false') {
return res.status(403).json({ error: 'demo mode disabled' });
}
const itemId = req.params.id;
const client = await pool.connect();
try {
await client.query('BEGIN');
const { rows } = await client.query(`SELECT * FROM items WHERE id = $1 FOR UPDATE`, [itemId]);
const item = rows[0];
if (!item) { await client.query('ROLLBACK'); return res.status(404).json({ error: 'not found' }); }
if (item.status === 'sold') { await client.query('ROLLBACK'); return res.status(409).json({ error: 'already sold' }); }
const { rows: updated } = await client.query(
`UPDATE items SET status = 'sold', sold_at = now() WHERE id = $1 RETURNING *`,
[itemId]
);
await client.query(
`INSERT INTO orders (item_id, customer_id, processor, processor_order_id, amount_cents, status, raw_event)
VALUES ($1, $2, 'demo', $3, $4, 'completed', $5)`,
[itemId, req.customerId || null, `demo-${Date.now()}`, item.price_cents, JSON.stringify({ demo: true })]
);
await client.query('COMMIT');
res.json({ status: 'sold', item: updated[0] });
} catch (err) {
await client.query('ROLLBACK');
console.error(err);
res.status(500).json({ error: 'internal error' });
} finally {
client.release();
}
});
export default router;
-156
View File
@@ -1,156 +0,0 @@
import { Router, Request, Response } from 'express';
import { pool } from '../db';
const router = Router();
const webhookRouter = Router();
const PAYPAL_BASE =
process.env.PAYPAL_ENV === 'live'
? 'https://api-m.paypal.com'
: 'https://api-m.sandbox.paypal.com';
const RESERVATION_MINUTES = parseInt(process.env.RESERVATION_MINUTES || '15', 10);
async function getAccessToken(): Promise<string> {
const auth = Buffer.from(
`${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
).toString('base64');
const resp = await fetch(`${PAYPAL_BASE}/v1/oauth2/token`, {
method: 'POST',
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'grant_type=client_credentials'
});
const data = await resp.json();
if (!resp.ok) throw new Error('paypal auth failed: ' + JSON.stringify(data));
return data.access_token;
}
router.post('/:id/create', async (req: Request, res: Response) => {
const itemId = req.params.id;
const client = await pool.connect();
try {
await client.query('BEGIN');
const { rows } = await client.query(`SELECT * FROM items WHERE id = $1 FOR UPDATE`, [itemId]);
const item = rows[0];
if (!item) { await client.query('ROLLBACK'); return res.status(404).json({ error: 'not found' }); }
if (item.status === 'sold') { await client.query('ROLLBACK'); return res.status(409).json({ error: 'already sold' }); }
if (item.status === 'reserved' && new Date(item.reserved_until) > new Date()) {
await client.query('ROLLBACK');
return res.status(409).json({ error: 'currently reserved by another checkout' });
}
const token = await getAccessToken();
const amount = (item.price_cents / 100).toFixed(2);
const orderResp = await fetch(`${PAYPAL_BASE}/v2/checkout/orders`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
intent: 'CAPTURE',
purchase_units: [{
custom_id: String(item.id),
description: item.name,
amount: { currency_code: process.env.SITE_CURRENCY || 'USD', value: amount }
}]
})
});
const order = await orderResp.json();
if (!orderResp.ok) { await client.query('ROLLBACK'); return res.status(502).json({ error: 'paypal order create failed', detail: order }); }
const reservedUntil = new Date(Date.now() + RESERVATION_MINUTES * 60 * 1000);
await client.query(
`UPDATE items SET status = 'reserved', reserved_until = $1, paypal_order_id = $2 WHERE id = $3`,
[reservedUntil, order.id, item.id]
);
await client.query('COMMIT');
res.json({ orderID: order.id });
} catch (err) {
await client.query('ROLLBACK');
console.error(err);
res.status(500).json({ error: 'internal error' });
} finally {
client.release();
}
});
router.post('/:id/capture', async (req: Request, res: Response) => {
const itemId = req.params.id;
const { orderID } = req.body;
const client = await pool.connect();
try {
const token = await getAccessToken();
const captureResp = await fetch(`${PAYPAL_BASE}/v2/checkout/orders/${orderID}/capture`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
});
const capture = await captureResp.json();
if (!captureResp.ok || capture.status !== 'COMPLETED') {
return res.status(502).json({ error: 'capture failed', detail: capture });
}
await client.query('BEGIN');
const { rows } = await client.query(
`UPDATE items SET status = 'sold', sold_at = now()
WHERE id = $1 AND paypal_order_id = $2 AND status != 'sold'
RETURNING *`,
[itemId, orderID]
);
const capturedAmount = capture.purchase_units?.[0]?.payments?.captures?.[0]?.amount?.value;
await client.query(
`INSERT INTO orders (item_id, customer_id, processor, processor_order_id, amount_cents, status, raw_event)
VALUES ($1, $2, 'paypal', $3, $4, 'completed', $5)`,
[itemId, req.customerId || null, orderID, Math.round(parseFloat(capturedAmount || '0') * 100), capture]
);
await client.query('COMMIT');
res.json({ status: 'sold', item: rows[0] || null });
} catch (err) {
await client.query('ROLLBACK');
console.error(err);
res.status(500).json({ error: 'internal error' });
} finally {
client.release();
}
});
webhookRouter.post('/', async (req: Request, res: Response) => {
try {
const token = await getAccessToken();
const verifyResp = await fetch(`${PAYPAL_BASE}/v1/notifications/verify-webhook-signature`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
auth_algo: req.headers['paypal-auth-algo'],
cert_url: req.headers['paypal-cert-url'],
transmission_id: req.headers['paypal-transmission-id'],
transmission_sig: req.headers['paypal-transmission-sig'],
transmission_time: req.headers['paypal-transmission-time'],
webhook_id: process.env.PAYPAL_WEBHOOK_ID,
webhook_event: req.body
})
});
const verification = await verifyResp.json();
if (verification.verification_status !== 'SUCCESS') {
console.warn('paypal webhook signature invalid');
return res.status(400).end();
}
const event = req.body;
if (event.event_type === 'PAYMENT.CAPTURE.COMPLETED') {
const itemId = event.resource?.custom_id;
if (itemId) {
await pool.query(
`UPDATE items SET status = 'sold', sold_at = now() WHERE id = $1 AND status != 'sold'`,
[itemId]
);
}
}
res.status(200).end();
} catch (err) {
console.error('webhook error', err);
res.status(500).end();
}
});
export { router, webhookRouter };