import { Router, Request, Response } from 'express'; import { pool } from '../db'; import { asyncRoute } from '../asyncRoute'; import { requireCustomer } from '../middleware/customerAuth'; import { validateAddress, uspsConfigured, UspsValidationResult } from '../usps'; /** * A whole `shipping_addresses` row. Every query here uses `SELECT *` or * `RETURNING *`, so one shape covers the file. Kept in step with the schema by * hand. */ interface ShippingAddressRow { id: number; customer_id: number; full_name: string; address_line1: string; address_line2: string | null; city: string; state: string; postal_code: string; country: string; is_default: boolean; usps_validated: boolean; // jsonb, and only ever handed back to the client — never read here. usps_standardized: unknown; created_at: Date; } const router = Router(); router.get('/', requireCustomer, asyncRoute(async (req: Request, res: Response) => { const { rows } = await pool.query( `SELECT * FROM shipping_addresses WHERE customer_id = $1 ORDER BY is_default DESC, created_at DESC`, [req.customerId] ); res.json(rows); })); router.post('/', requireCustomer, asyncRoute(async (req: Request, res: Response) => { const { fullName, addressLine1, addressLine2, city, state, postalCode, country, isDefault } = req.body; if (!fullName || !addressLine1 || !city || !state || !postalCode) { return res.status(400).json({ error: 'fullName, addressLine1, city, state, and postalCode are required' }); } let uspsResult: UspsValidationResult = { validated: false, deliverable: null, standardized: null }; if ((country || 'US') === 'US') { uspsResult = await validateAddress({ addressLine1, addressLine2, city, state, postalCode }); } const client = await pool.connect(); try { await client.query('BEGIN'); if (isDefault) { await client.query(`UPDATE shipping_addresses SET is_default = false WHERE customer_id = $1`, [req.customerId]); } const { rows } = await client.query( `INSERT INTO shipping_addresses (customer_id, full_name, address_line1, address_line2, city, state, postal_code, country, is_default, usps_validated, usps_standardized) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`, [ req.customerId, fullName, addressLine1, addressLine2 || null, city, state, postalCode, country || 'US', !!isDefault, uspsResult.deliverable === true, uspsResult.standardized ] ); await client.query('COMMIT'); res.status(201).json({ address: rows[0], uspsCheck: uspsResult, uspsConfigured: uspsConfigured() }); } catch (err) { await client.query('ROLLBACK'); console.error(err); res.status(500).json({ error: 'internal error' }); } finally { client.release(); } })); router.put('/:id', requireCustomer, asyncRoute(async (req: Request, res: Response) => { const { fullName, addressLine1, addressLine2, city, state, postalCode, country, isDefault } = req.body; const client = await pool.connect(); try { await client.query('BEGIN'); if (isDefault) { await client.query(`UPDATE shipping_addresses SET is_default = false WHERE customer_id = $1`, [req.customerId]); } const { rows } = await client.query( `UPDATE shipping_addresses SET full_name=$1, address_line1=$2, address_line2=$3, city=$4, state=$5, postal_code=$6, country=$7, is_default=$8, usps_validated = false, usps_standardized = NULL WHERE id=$9 AND customer_id=$10 RETURNING *`, [fullName, addressLine1, addressLine2 || null, city, state, postalCode, country || 'US', !!isDefault, req.params.id, req.customerId] ); if (!rows.length) { await client.query('ROLLBACK'); return res.status(404).json({ error: 'not found' }); } await client.query('COMMIT'); res.json(rows[0]); } catch (err) { await client.query('ROLLBACK'); console.error(err); res.status(500).json({ error: 'internal error' }); } finally { client.release(); } })); router.delete('/:id', requireCustomer, asyncRoute(async (req: Request, res: Response) => { await pool.query(`DELETE FROM shipping_addresses WHERE id = $1 AND customer_id = $2`, [req.params.id, req.customerId]); res.status(204).end(); })); router.post('/:id/set-default', requireCustomer, asyncRoute(async (req: Request, res: Response) => { const client = await pool.connect(); try { await client.query('BEGIN'); await client.query(`UPDATE shipping_addresses SET is_default = false WHERE customer_id = $1`, [req.customerId]); const { rows } = await client.query( `UPDATE shipping_addresses SET is_default = true WHERE id = $1 AND customer_id = $2 RETURNING *`, [req.params.id, req.customerId] ); await client.query('COMMIT'); res.json(rows[0]); } catch (err) { await client.query('ROLLBACK'); // Logged like the two catch blocks above it in this file, which this one // was simply missing. Without it a failed default-address change rolls // back and returns 500 leaving nothing behind to say why. console.error(err); res.status(500).json({ error: 'internal error' }); } finally { client.release(); } })); export default router;