import * as db from '../../db/entity.js'; function requireSelectedCompany(req) { const companyId = req.user.selectedCompanyId; if (companyId == null) { return { error: 'No company selected' }; } return { companyId }; } function entityErrorStatus(error) { if (error.message === 'No company selected') return 400; if (error.message === 'Not authorized') return 403; if (error.message === 'Company not found') return 404; if (error.message === 'Entity not found') return 404; if (error.message === 'Customer group not found') return 404; if (error.message === 'Name required') return 400; if (error.message === 'Number required') return 400; if (error.message === 'Number already in use') return 409; if (error.message === 'Invalid role') return 400; return 500; } /** * Registers entity routes for the authenticated user's selected company. * @param {import('express').Application} app * @param {string} [basePath=''] */ export function register(app, basePath = '') { /** * @openapi * /entity: * get: * tags: [Entity] * summary: List entities * description: | * Lists entities for the selected company. Optional `customergroupId` * filters to one group. Optional `role=customer` excludes Lieferanten/POS * groups (for quote/order pickers). `role` wins when both are set. * security: * - cookieAuth: [] * x-markdown: entity/list.md * parameters: * - in: query * name: customergroupId * required: false * schema: * type: integer * - in: query * name: role * required: false * schema: * type: string * enum: [customer] * responses: * 200: * description: Entity list * content: * application/json: * schema: * type: object * properties: * entities: * type: array * items: * $ref: '#/components/schemas/Entity' * 400: * description: No company selected, invalid customergroupId, or invalid role * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Company or customer group not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * post: * tags: [Entity] * summary: Create entity * description: | * Creates an entity in a company customer group and writes `entityhistory` (`Erstellt`). * Optional `number`: omit to allocate the next customer number (`type` entity). * security: * - cookieAuth: [] * x-markdown: entity/create.md * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [customergroupId, name] * properties: * customergroupId: * type: integer * name: * type: string * firstName: * type: string * nullable: true * number: * type: string * description: Customer number; omit to auto-allocate * example: * customergroupId: 1 * name: Müller * firstName: Anna * number: '10042' * responses: * 201: * description: Created entity * content: * application/json: * schema: * type: object * properties: * entity: * $ref: '#/components/schemas/Entity' * 400: * description: Name/number required or no company selected * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Company or customer group not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 409: * description: Number already in use * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ app.get(`${basePath}/entity`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const options = {}; if (Object.prototype.hasOwnProperty.call(req.query, 'customergroupId')) { const customergroupId = Number(req.query.customergroupId); if (!Number.isInteger(customergroupId) || customergroupId <= 0) { return res.status(400).json({ error: 'customergroupId is invalid' }); } options.customergroupId = customergroupId; } if (Object.prototype.hasOwnProperty.call(req.query, 'role')) { options.role = req.query.role; } try { const entities = await db.list(req.user.id, options); res.status(200).json({ entities }); } catch (error) { const status = entityErrorStatus(error); if (status === 500) console.error('List entities failed:', error); res.status(status).json({ error: error.message }); } }); app.post(`${basePath}/entity`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const body = req.body ?? {}; const data = { customergroupId: body.customergroupId, name: body.name, firstName: body.firstName, }; if (Object.prototype.hasOwnProperty.call(body, 'number')) { data.number = body.number; } try { const entity = await db.create(req.user.id, data); res.status(201).json({ entity }); } catch (error) { const status = entityErrorStatus(error); if (status === 500) console.error('Create entity failed:', error); res.status(status).json({ error: error.message }); } }); /** * @openapi * /entity/next-number: * get: * tags: [Entity] * summary: Peek next customer number * description: | * Returns the next customer number that would be allocated (`type` entity) * without consuming it. Use on the customer add form. * security: * - cookieAuth: [] * x-markdown: entity/next-number.md * responses: * 200: * description: Next number * content: * application/json: * schema: * type: object * required: [number] * properties: * number: * type: string * example: * number: '1' * 400: * description: No company selected * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Company not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ app.get(`${basePath}/entity/next-number`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } try { const number = await db.peekNextNumber(req.user.id); res.status(200).json({ number }); } catch (error) { const status = entityErrorStatus(error); if (status === 500) console.error('Peek next entity number failed:', error); res.status(status).json({ error: error.message }); } }); /** * @openapi * /entity/{id}: * get: * tags: [Entity] * summary: Get entity * description: Returns one entity for the selected company. * security: * - cookieAuth: [] * x-markdown: entity/get.md * parameters: * - in: path * name: id * required: true * schema: * type: integer * responses: * 200: * description: Entity * content: * application/json: * schema: * type: object * properties: * entity: * $ref: '#/components/schemas/Entity' * 400: * description: No company selected or invalid id * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Entity or company not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * patch: * tags: [Entity] * summary: Update entity * description: | * Patches fields that are present (`name`, `firstName`, `number`). * `number` is only changed when the key is present (no auto-allocate). * Writes `entityhistory` when values change. * security: * - cookieAuth: [] * x-markdown: entity/update.md * parameters: * - in: path * name: id * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * name: * type: string * firstName: * type: string * nullable: true * number: * type: string * nullable: true * example: * name: Müller * firstName: Anna * number: '10042' * responses: * 200: * description: Updated entity * content: * application/json: * schema: * type: object * properties: * entity: * $ref: '#/components/schemas/Entity' * 400: * description: Name/number required, invalid id, or no company selected * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Entity or company not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 409: * description: Number already in use * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * delete: * tags: [Entity] * summary: Delete entity * description: Deletes the entity (`entityhistory` cascades; related addresses/contacts cascade). * security: * - cookieAuth: [] * x-markdown: entity/delete.md * parameters: * - in: path * name: id * required: true * schema: * type: integer * responses: * 204: * description: Deleted * 400: * description: No company selected or invalid id * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Entity not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ app.get(`${basePath}/entity/:id`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const id = Number(req.params.id); if (!Number.isInteger(id) || id <= 0) { return res.status(400).json({ error: 'id is required' }); } try { const entity = await db.getById(req.user.id, id); res.status(200).json({ entity }); } catch (error) { const status = entityErrorStatus(error); if (status === 500) console.error('Get entity failed:', error); res.status(status).json({ error: error.message }); } }); app.patch(`${basePath}/entity/:id`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const id = Number(req.params.id); if (!Number.isInteger(id) || id <= 0) { return res.status(400).json({ error: 'id is required' }); } const body = req.body ?? {}; const patch = {}; if (Object.prototype.hasOwnProperty.call(body, 'name')) { patch.name = body.name; } if (Object.prototype.hasOwnProperty.call(body, 'firstName')) { patch.firstName = body.firstName; } if (Object.prototype.hasOwnProperty.call(body, 'number')) { patch.number = body.number; } try { const entity = await db.update(req.user.id, id, patch); res.status(200).json({ entity }); } catch (error) { const status = entityErrorStatus(error); if (status === 500) console.error('Update entity failed:', error); res.status(status).json({ error: error.message }); } }); app.delete(`${basePath}/entity/:id`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const id = Number(req.params.id); if (!Number.isInteger(id) || id <= 0) { return res.status(400).json({ error: 'id is required' }); } try { await db.remove(req.user.id, id); res.sendStatus(204); } catch (error) { const status = entityErrorStatus(error); if (status === 500) console.error('Delete entity failed:', error); res.status(status).json({ error: error.message }); } }); }