adamp 7068ea354e Fix bugs, harden validation, and improve robustness
P0 fixes:
- Fix OrderDetail product change overwriting product_id due to React
  state batching (single setItems call now updates both fields)
- Validate all :id route params via parseId helper; return 400 for
  invalid IDs instead of passing raw strings to SQLite
- Product/customer delete now checks for references first, returns
  409 Conflict instead of letting FK constraint produce 500

P1 fixes:
- Disallow quantity_on_hand in product PUT so all stock changes go
  through PATCH /stock (preserves audit trail)
- Add global Express error handler and unhandledRejection listener

P2 fixes:
- Validate report date params (YYYY-MM-DD format) and stock-history
  limit (positive integer, capped at 1000)
- Add jsonSafe() helper to api.js for safe 204 handling
- OrderNew setSubmitting now runs in finally block
- Login shows specific message for 429 rate limit, generic message
  for other auth failures

P3 fixes:
- Replace brittle try/catch ALTER TABLE with schema_version migration
  table and versioned migrations
- Fix OrderDetail useEffect missing dependency (useCallback + [load])

Also: expanded README with full production deployment instructions
(PM2, nginx, backups)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 21:35:53 -06:00

93 lines
3.5 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { getDb } = require('../db');
const { parseId } = require('../utils');
router.get('/', (req, res) => {
try {
const db = getDb();
const rows = db.prepare('SELECT * FROM customers ORDER BY name').all();
res.json(rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/:id', (req, res) => {
try {
const id = parseId(req.params.id);
if (!id) return res.status(400).json({ error: 'Invalid customer ID' });
const db = getDb();
const row = db.prepare('SELECT * FROM customers WHERE id = ?').get(id);
if (!row) return res.status(404).json({ error: 'Customer not found' });
res.json(row);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/', (req, res) => {
try {
const db = getDb();
const { name, phone, email, address, notes } = req.body;
if (!name || name.trim() === '') {
return res.status(400).json({ error: 'Name is required' });
}
const result = db.prepare(
'INSERT INTO customers (name, phone, email, address, notes) VALUES (?, ?, ?, ?, ?)'
).run(
name.trim(),
phone ? String(phone).trim() : null,
email ? String(email).trim() : null,
address ? String(address).trim() : null,
notes ? String(notes).trim() : null
);
const row = db.prepare('SELECT * FROM customers WHERE id = ?').get(result.lastInsertRowid);
res.status(201).json(row);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.put('/:id', (req, res) => {
try {
const id = parseId(req.params.id);
if (!id) return res.status(400).json({ error: 'Invalid customer ID' });
const db = getDb();
const existing = db.prepare('SELECT * FROM customers WHERE id = ?').get(id);
if (!existing) return res.status(404).json({ error: 'Customer not found' });
const { name, phone, email, address, notes } = req.body;
const n = name !== undefined ? name.trim() : existing.name;
const ph = phone !== undefined ? (phone ? String(phone).trim() : null) : existing.phone;
const e = email !== undefined ? (email ? String(email).trim() : null) : existing.email;
const a = address !== undefined ? (address ? String(address).trim() : null) : existing.address;
const no = notes !== undefined ? (notes ? String(notes).trim() : null) : existing.notes;
if (!n) return res.status(400).json({ error: 'Name is required' });
db.prepare(
'UPDATE customers SET name = ?, phone = ?, email = ?, address = ?, notes = ? WHERE id = ?'
).run(n, ph, e, a, no, id);
const row = db.prepare('SELECT * FROM customers WHERE id = ?').get(id);
res.json(row);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.delete('/:id', (req, res) => {
try {
const id = parseId(req.params.id);
if (!id) return res.status(400).json({ error: 'Invalid customer ID' });
const db = getDb();
const existing = db.prepare('SELECT id FROM customers WHERE id = ?').get(id);
if (!existing) return res.status(404).json({ error: 'Customer not found' });
const refs = db.prepare('SELECT COUNT(*) as count FROM orders WHERE customer_id = ?').get(id);
if (refs.count > 0) return res.status(409).json({ error: 'Cannot delete customer that has existing orders' });
db.prepare('DELETE FROM customers WHERE id = ?').run(id);
res.status(204).send();
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;