Compare commits

...

2 Commits

Author SHA1 Message Date
seb
ea51f30a8d u 2026-10-01 22:17:17 +02:00
seb
6b58947064 u 2026-10-01 21:28:28 +02:00
5 changed files with 90 additions and 8 deletions

35
AGENTS.md Normal file
View File

@@ -0,0 +1,35 @@
# AGENTS.md — dashback (Express API)
ESM (`type: module`), Express 5, Node. Depends on `libdashstore` via `file:../dashdbschema`.
## Architecture
- Entry: `src/index.js` → `src/api/index.js` (`startApi`/`stopApi`, `loadRoutes()` walks `src/api/routes/**/*.js` → `register(app, basePath)`).
- Each route file owns its `@openapi` JSDoc + `*ErrorStatus(err)` mapper + `requireSelectedCompany(req)`. Source of truth is code.
- DB: `src/db/store.js` lazy singleton provider+store (timing logs `libstore <ns>.<method>`); `src/db/<domain>.js` are thin wrappers around `store.<ns>.<method>`.
- Config: `src/config/env.js` (`.env`; `PORT` 4208, `HTTPS_PORT`, `GOOGLE_CLIENT_ID`, `JWT_SECRET`, `CORS_ORIGINS`, `DB_PROVIDER=mssql|sqlite`, `SQLITE_PATH`, `MSSQL_*`).
- Auth: `src/api/middleware/auth.js` (Google SSO + JWT cookie, `req.user.{id,selectedCompanyId}`); allowlist via `scripts/manageUsers.js`.
- SSE: `src/api/sse.js` (`emit`), `routes/events.js`. Stock adjust emits `stock`, `ledger`, `articles`, `article-<sku>`.
- `src/v1/` legacy POS compat — don't touch unless task is POS. `quixpos2jtl/` is a separate Electron/CLI bridge — ignore unless task mentions JTL.
- Docs: generated, never hand-edit — see below.
## Build / Test / Docs
```bash
cp .env.example .env && npm install
npm start # PORT 4208 (HTTPS 8443)
npm run docs:api # regenerates docs/api/routes.md + docs/api/routes/ + openapi.json
npm run manage-users # email allowlist
```
- No `npm test`. Verify with syntax check or a self-exiting run on a **different port**; never leave a listener running — user keeps their own dev server.
- Store contract lives in sibling repo: `../dashdbschema/API.md` + `API.<module>.md`, `../dashdbschema/README.md`. Read-only.
## Conventions
- Sibling repos (`../dashdbschema`, `../dashfront`) are **read-only**: may read docs/code + `git -C ../<repo> log --oneline -20`, never write/install/start. Missing contract → ask human, don't guess. No fallbacks/defaults. See `.cursor/rules/sibling-repos.mdc`, `.cursor/rules/dont-start-the-API-server.mdc`.
- After any route change run `npm run docs:api`. Docs index: `docs/api/README.md`.
- HTTP mapping per route file: `No company selected→400`, `Not authorized→403`, `*not found→404`, validation (`includes()`)→400, uniqueness/in-use→409, else 500 + `console.error`. `Stock tracking is disabled…→409`.
- Keep error strings stable/greppable — mapping depends on exact messages from `libdashstore`.
- Stock/ledger invariants belong in `dashdbschema/src/store/`, not routes, so both `stock.adjust` and direct `ledger.add` are covered.
- Full backend/store conventions: `.agents/skills/project-overview/SKILL.md`.

View File

@@ -27,6 +27,7 @@ _No content_
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Delivery not found — `{"error":"…"}` |
| 409 | Delivery in use in ledger — `{"error":"…"}` |
---

View File

@@ -102,6 +102,7 @@ Updated delivery
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Delivery, supplier, or company not found — `{"error":"…"}` |
| 409 | Delivery lines already booked on ledger — `{"error":"…"}` |
---

View File

@@ -20,8 +20,12 @@ function captureResponseBody(res) {
const originalSend = res.send.bind(res);
res.json = (payload) => {
body = payload;
return originalJson(payload);
let out = payload;
if (res.statusCode >= 500 && payload && typeof payload === 'object' && 'error' in payload) {
out = { ...payload, error: 'Internal server error' };
}
body = out;
return originalJson(out);
};
res.send = (payload) => {

View File

@@ -172,6 +172,15 @@ function deliveryErrorStatus(error) {
if (error.message === 'Invalid JSON in "data" field') return 400;
if (error.message === 'Only PDF or text files are accepted as attachments') return 400;
if (error.message?.includes('father article') || error.message?.includes('A father article')) return 400;
if (
error.message === 'Delivery in use in ledger' ||
error.message === 'Delivery lines already booked on ledger' ||
error.message?.includes('FOREIGN KEY constraint failed') ||
error.message?.includes('foreign key constraint') ||
error.message?.includes('REFERENCE constraint') ||
error.code === 'SQLITE_CONSTRAINT_FOREIGNKEY' ||
error.number === 547
) return 409;
return 500;
}
@@ -348,7 +357,7 @@ export function register(app, basePath = '') {
res.locals.error = error;
const status = deliveryErrorStatus(error);
if (status === 500) console.error('List deliveries failed:', error);
res.status(status).json({ error: error.message });
res.status(status).json({ error: status === 500 ? 'Internal server error' : error.message });
}
});
@@ -399,7 +408,7 @@ export function register(app, basePath = '') {
console.error('[delivery diag] Failed to dump payload:', logErr.message);
}
}
res.status(status).json({ error: error.message });
res.status(status).json({ error: status === 500 ? 'Internal server error' : error.message });
}
});
@@ -550,6 +559,12 @@ export function register(app, basePath = '') {
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Delivery lines already booked on ledger
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* delete:
* tags: [Delivery]
* summary: Delete delivery
@@ -591,6 +606,12 @@ export function register(app, basePath = '') {
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Delivery in use in ledger
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.get(`${basePath}/delivery/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
@@ -610,7 +631,7 @@ export function register(app, basePath = '') {
res.locals.error = error;
const status = deliveryErrorStatus(error);
if (status === 500) console.error('Get delivery failed:', error);
res.status(status).json({ error: error.message });
res.status(status).json({ error: status === 500 ? 'Internal server error' : error.message });
}
});
@@ -656,7 +677,21 @@ export function register(app, basePath = '') {
if (status === 500) {
logStoreError('Update delivery failed', error);
}
res.status(status).json({ error: error.message });
let errorMessage = error.message;
if (status === 500) {
errorMessage = 'Internal server error';
} else if (
status === 409 ||
error.message === 'Delivery lines already booked on ledger' ||
error.message?.includes('FOREIGN KEY constraint failed') ||
error.message?.includes('foreign key constraint') ||
error.message?.includes('REFERENCE constraint') ||
error.code === 'SQLITE_CONSTRAINT_FOREIGNKEY' ||
error.number === 547
) {
errorMessage = 'Delivery lines are already booked on the ledger and cannot be modified';
}
res.status(status).json({ error: errorMessage });
}
});
@@ -678,7 +713,13 @@ export function register(app, basePath = '') {
res.locals.error = error;
const status = deliveryErrorStatus(error);
if (status === 500) console.error('Delete delivery failed:', error);
res.status(status).json({ error: error.message });
let errorMessage = error.message;
if (status === 500) {
errorMessage = 'Internal server error';
} else if (status === 409) {
errorMessage = 'Delivery in use in ledger';
}
res.status(status).json({ error: errorMessage });
}
});
@@ -822,7 +863,7 @@ export function register(app, basePath = '') {
res.locals.error = error;
const status = deliveryErrorStatus(error);
if (status === 500) console.error('Get delivery file failed:', error);
res.status(status).json({ error: error.message });
res.status(status).json({ error: status === 500 ? 'Internal server error' : error.message });
}
};