import * as db from '../../db/supplier.js'; function requireSelectedCompany(req) { const companyId = req.user.selectedCompanyId; if (companyId == null) { return { error: 'No company selected' }; } return { companyId }; } function supplierErrorStatus(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 === 'Supplier not found') return 404; if (error.message === 'Supplier required') return 400; if (error.message === 'Name required') return 400; return 500; } function parsePositiveInt(value, field) { const id = Number(value); if (!Number.isInteger(id) || id <= 0) { return { error: `${field} is required` }; } return { id }; } function parseSkus(source) { if (source == null) return undefined; if (Array.isArray(source)) return source; if (typeof source === 'string') { const trimmed = source.trim(); if (!trimmed) return []; if (trimmed.startsWith('[') && trimmed.endsWith(']')) { try { const parsed = JSON.parse(trimmed); if (Array.isArray(parsed)) return parsed; } catch { // fall through } } if (trimmed.includes(',')) { return trimmed.split(',').map((s) => s.trim()).filter(Boolean); } return [trimmed]; } return undefined; } /** * Registers supplier routes for the authenticated user's selected company. * @param {import('express').Application} app * @param {string} [basePath=''] */ export function register(app, basePath = '') { /** * @openapi * /supplier: * get: * tags: [Supplier] * summary: List suppliers * description: | * Lists suppliers (`entity` rows in the company `Lieferanten` customergroup) * for the authenticated user's selected company. * security: * - cookieAuth: [] * x-markdown: supplier/list.md * responses: * 200: * description: Supplier list * content: * application/json: * schema: * type: object * properties: * suppliers: * type: array * items: * $ref: '#/components/schemas/Supplier' * 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' * post: * tags: [Supplier] * summary: Create supplier * description: | * Creates a supplier in the company `Lieferanten` group (created on first use). * security: * - cookieAuth: [] * x-markdown: supplier/create.md * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [name] * properties: * name: * type: string * example: * name: Großhandel Müller * responses: * 201: * description: Created supplier * content: * application/json: * schema: * type: object * properties: * supplier: * $ref: '#/components/schemas/Supplier' * 400: * description: Name 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 not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ app.get(`${basePath}/supplier`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } try { const suppliers = await db.list(req.user.id); res.status(200).json({ suppliers }); } catch (error) { const status = supplierErrorStatus(error); if (status === 500) console.error('List suppliers failed:', error); res.status(status).json({ error: error.message }); } }); app.post(`${basePath}/supplier`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const { name } = req.body ?? {}; try { const supplier = await db.create(req.user.id, { name }); res.status(201).json({ supplier }); } catch (error) { const status = supplierErrorStatus(error); if (status === 500) console.error('Create supplier failed:', error); res.status(status).json({ error: error.message }); } }); /** * @openapi * /supplier/{id}: * patch: * tags: [Supplier] * summary: Update supplier * description: Renames a supplier for the selected company. * security: * - cookieAuth: [] * x-markdown: supplier/update.md * parameters: * - in: path * name: id * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [name] * properties: * name: * type: string * example: * name: Großhandel Müller GmbH * responses: * 200: * description: Updated supplier * content: * application/json: * schema: * type: object * properties: * supplier: * $ref: '#/components/schemas/Supplier' * 400: * description: Name 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: Supplier or company not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * delete: * tags: [Supplier] * summary: Delete supplier * description: | * Deletes the supplier entity for the selected company. * Related `entityhistory` cascades; `manifest.entity_id` is set null. * security: * - cookieAuth: [] * x-markdown: supplier/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: Supplier not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ app.patch(`${basePath}/supplier/: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 { name } = req.body ?? {}; try { const supplier = await db.update(req.user.id, id, { name }); res.status(200).json({ supplier }); } catch (error) { const status = supplierErrorStatus(error); if (status === 500) console.error('Update supplier failed:', error); res.status(status).json({ error: error.message }); } }); app.delete(`${basePath}/supplier/: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 = supplierErrorStatus(error); if (status === 500) console.error('Delete supplier failed:', error); res.status(status).json({ error: error.message }); } }); /** * @openapi * /supplier/{id}/articles: * get: * tags: [Supplier] * summary: List supplier articles by SKUs * description: | * Lists article-supplier assignments for the specified supplier (entity ID), * optionally filtered by a list of SKUs (matching either article SKU or supplier SKU). * Includes linked article details (name, slug, photos/articlepic, eans/articleean). * security: * - cookieAuth: [] * x-markdown: supplier/articles.get.md * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Supplier entity ID * - in: query * name: skus * required: false * schema: * oneOf: * - type: array * items: * type: string * - type: string * description: Optional SKU or comma-separated SKUs to filter by * responses: * 200: * description: List of supplier articles * content: * application/json: * schema: * type: object * properties: * articles: * type: array * items: * $ref: '#/components/schemas/SupplierArticle' * 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: Supplier or company not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * post: * tags: [Supplier] * summary: Query supplier articles by SKUs (bulk) * description: | * Queries article-supplier assignments for the specified supplier (entity ID) with a * JSON list of SKUs in the request body, avoiding URL length limits for large SKU lists. * Includes linked article details (name, slug, photos/articlepic, eans/articleean). * security: * - cookieAuth: [] * x-markdown: supplier/articles.post.md * parameters: * - in: path * name: id * required: true * schema: * type: integer * description: Supplier entity ID * requestBody: * required: false * content: * application/json: * schema: * type: object * properties: * skus: * type: array * items: * type: string * description: List of SKUs to filter by (empty array returns empty list) * example: * skus: ['SKU-1', 'SKU-2'] * responses: * 200: * description: List of supplier articles * content: * application/json: * schema: * type: object * properties: * articles: * type: array * items: * $ref: '#/components/schemas/SupplierArticle' * 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: Supplier or company not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ async function handleGetSupplierArticles(req, res) { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const idParsed = parsePositiveInt(req.params.id, 'id'); if (idParsed.error) { return res.status(400).json({ error: idParsed.error }); } const skus = parseSkus(req.query.skus ?? req.query.sku); try { const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus); res.status(200).json({ articles }); } catch (error) { const status = supplierErrorStatus(error); if (status === 500) console.error('List supplier articles failed:', error); res.status(status).json({ error: error.message }); } } async function handlePostSupplierArticles(req, res) { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const idParsed = parsePositiveInt(req.params.id, 'id'); if (idParsed.error) { return res.status(400).json({ error: idParsed.error }); } const skus = parseSkus(req.body?.skus ?? req.body?.sku); try { const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus); res.status(200).json({ articles }); } catch (error) { const status = supplierErrorStatus(error); if (status === 500) console.error('Query supplier articles failed:', error); res.status(status).json({ error: error.message }); } } app.get(`${basePath}/supplier/:id/articles`, handleGetSupplierArticles); app.get(`${basePath}/supplier/:id/article`, handleGetSupplierArticles); app.post(`${basePath}/supplier/:id/articles`, handlePostSupplierArticles); app.post(`${basePath}/supplier/:id/article`, handlePostSupplierArticles); }