diff --git a/docs/api/README.md b/docs/api/README.md index 06b50bf..59d1b02 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -183,8 +183,10 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/unit/delete.md`](routes/unit/delete.md) | `DELETE /unit` | | [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` | | [`routes/unit/upsert.md`](routes/unit/upsert.md) | `PUT /unit` | +| [`routes/v1/category.md`](routes/v1/category.md) | `GET /v1/category` | | [`routes/pos/client.md`](routes/pos/client.md) | `GET /v1/client` | | [`routes/pos/init.md`](routes/pos/init.md) | `GET /v1/init` | +| [`routes/v1/product.md`](routes/v1/product.md) | `GET /v1/product` | | [`routes/warehouse/list.md`](routes/warehouse/list.md) | `GET /warehouse` | | [`routes/warehouse/create.md`](routes/warehouse/create.md) | `POST /warehouse` | | [`routes/warehouse/order.md`](routes/warehouse/order.md) | `PUT /warehouse/order` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 73eb382..3fee8f1 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -13576,6 +13576,57 @@ } } }, + "/v1/product": { + "get": { + "summary": "JTL-POS product catalog synchronization", + "description": "Returns product catalog with prices, stock, variants, and image hashes modified after lastChangedProduct cursor.", + "tags": [ + "POS Sync" + ], + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "lastChangedProduct", + "schema": { + "type": "string" + }, + "description": "Unix timestamp cursor of last product sync" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 50 + }, + "description": "Maximum number of products to return" + } + ], + "responses": { + "200": { + "description": "Array of product objects for JTL-POS", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + } + }, "/v1/init": { "get": { "tags": [ @@ -13841,6 +13892,57 @@ } } } + }, + "/v1/category": { + "get": { + "summary": "JTL-POS category synchronization", + "description": "Returns category hierarchy modified after lastChangedCategory cursor for the authenticated POS register.", + "tags": [ + "POS Sync" + ], + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "lastChangedCategory", + "schema": { + "type": "string" + }, + "description": "Unix timestamp cursor of last category sync" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 50 + }, + "description": "Maximum number of categories to return" + } + ], + "responses": { + "200": { + "description": "Array of category objects for JTL-POS", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + } } }, "components": { diff --git a/docs/api/routes/v1/category.md b/docs/api/routes/v1/category.md new file mode 100644 index 0000000..b67e2f5 --- /dev/null +++ b/docs/api/routes/v1/category.md @@ -0,0 +1,34 @@ +# `GET /v1/category` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +JTL-POS category synchronization + +Returns category hierarchy modified after lastChangedCategory cursor for the authenticated POS register. + +## Query parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `lastChangedCategory` | no | string | Unix timestamp cursor of last category sync | +| `limit` | no | integer | Maximum number of categories to return | + +## Response `200` + +Array of category objects for JTL-POS + +```json +[ + {} +] +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 401 | Unauthorized | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/v1/product.md b/docs/api/routes/v1/product.md new file mode 100644 index 0000000..f352533 --- /dev/null +++ b/docs/api/routes/v1/product.md @@ -0,0 +1,34 @@ +# `GET /v1/product` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +JTL-POS product catalog synchronization + +Returns product catalog with prices, stock, variants, and image hashes modified after lastChangedProduct cursor. + +## Query parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `lastChangedProduct` | no | string | Unix timestamp cursor of last product sync | +| `limit` | no | integer | Maximum number of products to return | + +## Response `200` + +Array of product objects for JTL-POS + +```json +[ + {} +] +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 401 | Unauthorized | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/src/api/routes/stock.js b/src/api/routes/stock.js index e9cdda9..b114a10 100644 --- a/src/api/routes/stock.js +++ b/src/api/routes/stock.js @@ -38,6 +38,13 @@ function emitStockEvents(companyId, body, result) { } } } + if (Array.isArray(result?.lines)) { + for (const line of result.lines) { + if (line?.sku) { + emit(`article-${line.sku}`, { companyId }); + } + } + } } /** diff --git a/src/db/posterminal.js b/src/db/posterminal.js index 66b84b0..51b1117 100644 --- a/src/db/posterminal.js +++ b/src/db/posterminal.js @@ -85,3 +85,13 @@ export async function getMaxOrderIdCount(params) { return store.posterminal.getMaxOrderIdCount(params); } +export async function getCategoryList(params) { + const store = await getStore(); + return store.posterminal.getCategoryList(params); +} + +export async function getProductList(params) { + const store = await getStore(); + return store.posterminal.getProductList(params); +} + diff --git a/src/v1/index.js b/src/v1/index.js index 0c31687..ef3e466 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -2,6 +2,8 @@ import { Router } from 'express'; import { posAuthMiddleware } from './middleware/posAuth.js'; import { handleClient } from './routes/client.js'; import { handleInit } from './routes/init.js'; +import { handleCategory } from './routes/category.js'; +import { handleProduct } from './routes/product.js'; const router = Router(); @@ -11,5 +13,9 @@ router.get('/client', handleClient); // POS sync initialization counts router.get('/init', posAuthMiddleware, handleInit); +// POS master data synchronization +router.get('/category', posAuthMiddleware, handleCategory); +router.get('/product', posAuthMiddleware, handleProduct); + export default router; diff --git a/src/v1/routes/category.js b/src/v1/routes/category.js new file mode 100644 index 0000000..8730d8a --- /dev/null +++ b/src/v1/routes/category.js @@ -0,0 +1,67 @@ +import { getCategoryList } from '../../db/posterminal.js'; + +function formatServerTimestamp() { + const d = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +} + +/** + * @openapi + * /v1/category: + * get: + * summary: JTL-POS category synchronization + * description: Returns category hierarchy modified after lastChangedCategory cursor for the authenticated POS register. + * tags: + * - POS Sync + * security: + * - BearerAuth: [] + * parameters: + * - in: query + * name: lastChangedCategory + * schema: + * type: string + * description: Unix timestamp cursor of last category sync + * - in: query + * name: limit + * schema: + * type: integer + * default: 50 + * description: Maximum number of categories to return + * responses: + * 200: + * description: Array of category objects for JTL-POS + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * 401: + * description: Unauthorized + */ +export async function handleCategory(req, res, next) { + try { + const posterminal = req.posterminal; + const cursor = Number(req.query.lastChangedCategory) || 0; + const limit = Number(req.query.limit) || 20; + + const categories = await getCategoryList({ + posterminalId: posterminal.id, + cursor, + limit, + }); + + const timestamp = formatServerTimestamp(); + + res.json( + categories.map((category) => ({ + ...category, + updated_at: timestamp, + created_at: timestamp, + })) + ); + } catch (err) { + next(err); + } +} diff --git a/src/v1/routes/product.js b/src/v1/routes/product.js new file mode 100644 index 0000000..f37d56d --- /dev/null +++ b/src/v1/routes/product.js @@ -0,0 +1,104 @@ +import { getProductList } from '../../db/posterminal.js'; + +const STATIC_DEFAULTS = { + sort: '0', + p_price: '0.00', + discountable: '0', + deposit: '0', + discount: '', + d_price: '0.0', + tax_rate2: '', + use_in_out_tax: '0', + barcode: null, + use_stock: '0', + q_div: '0', + quantity: '0', + unit: null, + single_bookable: '0', + annotation: '', + status: '0', + tags: '', + is_parent: '0', + parent: '0', + variants: '', + print_kitchen_receipt: '0', + deposit_name: '', + updated_at: '0001-01-01 00:00:00', + isCompositeProduct: '0', + attributes: [], + configurationGroups: '', + options: null, + hasBestBeforeDate: '0', + hasLotNumber: '0', + hasSerialNumber: '0', + PLU: '', + short_description: '', + minStock: '0', + container: [], + reservedQuantity: '0.00', + deliveryDetails: [], + isbn: '', + manufacturerName: null, + han: null, + productType: '0', + voucherData: null, + inputPrice: '0', + inputQuantity: '0', +}; + +/** + * @openapi + * /v1/product: + * get: + * summary: JTL-POS product catalog synchronization + * description: Returns product catalog with prices, stock, variants, and image hashes modified after lastChangedProduct cursor. + * tags: + * - POS Sync + * security: + * - BearerAuth: [] + * parameters: + * - in: query + * name: lastChangedProduct + * schema: + * type: string + * description: Unix timestamp cursor of last product sync + * - in: query + * name: limit + * schema: + * type: integer + * default: 50 + * description: Maximum number of products to return + * responses: + * 200: + * description: Array of product objects for JTL-POS + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * 401: + * description: Unauthorized + */ +export async function handleProduct(req, res, next) { + try { + const posterminal = req.posterminal; + const cursor = Number(req.query.lastChangedProduct) || 0; + const limit = Number(req.query.limit) || 20; + + const products = await getProductList({ + posterminalId: posterminal.id, + cursor, + limit, + }); + + res.json( + products.map((product) => ({ + ...STATIC_DEFAULTS, + ...product, + })) + ); + } catch (err) { + next(err); + } +}