u
This commit is contained in:
@@ -184,6 +184,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
|||||||
| [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` |
|
| [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` |
|
||||||
| [`routes/unit/upsert.md`](routes/unit/upsert.md) | `PUT /unit` |
|
| [`routes/unit/upsert.md`](routes/unit/upsert.md) | `PUT /unit` |
|
||||||
| [`routes/pos/client.md`](routes/pos/client.md) | `GET /v1/client` |
|
| [`routes/pos/client.md`](routes/pos/client.md) | `GET /v1/client` |
|
||||||
|
| [`routes/pos/init.md`](routes/pos/init.md) | `GET /v1/init` |
|
||||||
| [`routes/warehouse/list.md`](routes/warehouse/list.md) | `GET /warehouse` |
|
| [`routes/warehouse/list.md`](routes/warehouse/list.md) | `GET /warehouse` |
|
||||||
| [`routes/warehouse/create.md`](routes/warehouse/create.md) | `POST /warehouse` |
|
| [`routes/warehouse/create.md`](routes/warehouse/create.md) | `POST /warehouse` |
|
||||||
| [`routes/warehouse/order.md`](routes/warehouse/order.md) | `PUT /warehouse/order` |
|
| [`routes/warehouse/order.md`](routes/warehouse/order.md) | `PUT /warehouse/order` |
|
||||||
|
|||||||
@@ -8804,7 +8804,7 @@
|
|||||||
"Category"
|
"Category"
|
||||||
],
|
],
|
||||||
"summary": "Articles in category",
|
"summary": "Articles in category",
|
||||||
"description": "Returns articles in a category the user is authorized for.",
|
"description": "Returns articles in a category the user is authorized for (omits child articles of a father article). Each article includes `childCount` (the number of its child articles).",
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"cookieAuth": []
|
"cookieAuth": []
|
||||||
@@ -8834,6 +8834,62 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"sku": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"net": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"taxRate": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"track_stock": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"optionsCount": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"articleListCount": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"childCount": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of child articles belonging to this article"
|
||||||
|
},
|
||||||
|
"fullHash": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"thumbW": {
|
||||||
|
"type": "integer",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"thumbH": {
|
||||||
|
"type": "integer",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"thumbHash": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"brand": {
|
||||||
|
"type": "object",
|
||||||
|
"nullable": true,
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13016,7 +13072,7 @@
|
|||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"summary": "Set Stückliste (bundle items) for an article",
|
"summary": "Set Stückliste (bundle items) for an article",
|
||||||
"description": "Configures the article as a Stückliste covering a list of count * article. An article that is a father or has options cannot be a Stückliste. Pass an empty array to remove the Stückliste.",
|
"description": "Configures the article as a Stückliste covering a list of count * article. A father article cannot be a Stückliste. Can be combined with options. Pass an empty array to remove the Stückliste.",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Article"
|
"Article"
|
||||||
],
|
],
|
||||||
@@ -13172,7 +13228,7 @@
|
|||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"summary": "Set options list for an article",
|
"summary": "Set options list for an article",
|
||||||
"description": "Configures options for an article. Some options can be mandated (come with the article in any case). An article that is a father or a Stückliste cannot have options. Pass an empty array to remove options.",
|
"description": "Configures options for an article. Some options can be mandated (come with the article in any case). A father article cannot have options. Can be combined with a Stückliste. Pass an empty array to remove options.",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Article"
|
"Article"
|
||||||
],
|
],
|
||||||
@@ -13520,6 +13576,164 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/init": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"POS"
|
||||||
|
],
|
||||||
|
"summary": "POS sync initialization counts",
|
||||||
|
"description": "Returns entity change counts since the provided `lastChanged*` unix timestamp cursors\nfor the authenticated POS terminal (`Authorization: Bearer <authToken>`).\n",
|
||||||
|
"x-markdown": "pos/init.md",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "header",
|
||||||
|
"name": "Authorization",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "POS device authToken obtained during pairing (`Bearer <token>` or `<token>`)",
|
||||||
|
"example": "Bearer c30099b66ec1fcb9ccd05795423c94e8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "lastChangedProduct",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"description": "Unix timestamp of last synced product update",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "lastChangedCategory",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"description": "Unix timestamp of last synced category update",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "lastChangedCustomer",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"description": "Unix timestamp of last synced customer update",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "lastChangedCustomerGroup",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"description": "Unix timestamp of last synced customer group update",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "lastChangedCompositeProduct",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"description": "Unix timestamp of last synced composite product update",
|
||||||
|
"example": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "lastChangedDeletedEntity",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"description": "Unix timestamp of last synced deleted entity",
|
||||||
|
"example": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Sync initialization counts",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1.10.12.0"
|
||||||
|
},
|
||||||
|
"product_count": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "19"
|
||||||
|
},
|
||||||
|
"category_count": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "4"
|
||||||
|
},
|
||||||
|
"customer_count": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "0"
|
||||||
|
},
|
||||||
|
"customerGroup_count": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1"
|
||||||
|
},
|
||||||
|
"compositeProduct_count": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "0"
|
||||||
|
},
|
||||||
|
"configurationGroup_count": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "0"
|
||||||
|
},
|
||||||
|
"configurationItem_count": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "0"
|
||||||
|
},
|
||||||
|
"deletedEntity_count": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "0"
|
||||||
|
},
|
||||||
|
"max_orderId_count": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Missing or invalid POS Authorization token",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Authorization has been denied for this request."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/client": {
|
"/v1/client": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
|
|||||||
|
|
||||||
Set Stückliste (bundle items) for an article
|
Set Stückliste (bundle items) for an article
|
||||||
|
|
||||||
Configures the article as a Stückliste covering a list of count * article. An article that is a father or has options cannot be a Stückliste. Pass an empty array to remove the Stückliste.
|
Configures the article as a Stückliste covering a list of count * article. A father article cannot be a Stückliste. Can be combined with options. Pass an empty array to remove the Stückliste.
|
||||||
|
|
||||||
## Path parameters
|
## Path parameters
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
|
|||||||
|
|
||||||
Set options list for an article
|
Set options list for an article
|
||||||
|
|
||||||
Configures options for an article. Some options can be mandated (come with the article in any case). An article that is a father or a Stückliste cannot have options. Pass an empty array to remove options.
|
Configures options for an article. Some options can be mandated (come with the article in any case). A father article cannot have options. Can be combined with a Stückliste. Pass an empty array to remove options.
|
||||||
|
|
||||||
## Path parameters
|
## Path parameters
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
|
|||||||
|
|
||||||
Articles in category
|
Articles in category
|
||||||
|
|
||||||
Returns articles in a category the user is authorized for.
|
Returns articles in a category the user is authorized for (omits child articles of a father article). Each article includes `childCount` (the number of its child articles).
|
||||||
|
|
||||||
## Path parameters
|
## Path parameters
|
||||||
|
|
||||||
@@ -19,7 +19,23 @@ Articles list
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"articles": [
|
"articles": [
|
||||||
{}
|
{
|
||||||
|
"id": 1,
|
||||||
|
"sku": "…",
|
||||||
|
"name": "…",
|
||||||
|
"slug": "…",
|
||||||
|
"net": 1,
|
||||||
|
"taxRate": 1,
|
||||||
|
"track_stock": true,
|
||||||
|
"optionsCount": 1,
|
||||||
|
"articleListCount": 1,
|
||||||
|
"childCount": 1,
|
||||||
|
"fullHash": "…",
|
||||||
|
"thumbW": 1,
|
||||||
|
"thumbH": 1,
|
||||||
|
"thumbHash": "…",
|
||||||
|
"brand": {}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
49
docs/api/routes/pos/init.md
Normal file
49
docs/api/routes/pos/init.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# `GET /v1/init`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
POS sync initialization counts
|
||||||
|
|
||||||
|
Returns entity change counts since the provided `lastChanged*` unix timestamp cursors
|
||||||
|
for the authenticated POS terminal (`Authorization: Bearer <authToken>`).
|
||||||
|
|
||||||
|
|
||||||
|
## Query parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `lastChangedProduct` | no | integer | Unix timestamp of last synced product update |
|
||||||
|
| `lastChangedCategory` | no | integer | Unix timestamp of last synced category update |
|
||||||
|
| `lastChangedCustomer` | no | integer | Unix timestamp of last synced customer update |
|
||||||
|
| `lastChangedCustomerGroup` | no | integer | Unix timestamp of last synced customer group update |
|
||||||
|
| `lastChangedCompositeProduct` | no | integer | Unix timestamp of last synced composite product update |
|
||||||
|
| `lastChangedDeletedEntity` | no | integer | Unix timestamp of last synced deleted entity |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Sync initialization counts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.10.12.0",
|
||||||
|
"product_count": "19",
|
||||||
|
"category_count": "4",
|
||||||
|
"customer_count": "0",
|
||||||
|
"customerGroup_count": "1",
|
||||||
|
"compositeProduct_count": "0",
|
||||||
|
"configurationGroup_count": "0",
|
||||||
|
"configurationItem_count": "0",
|
||||||
|
"deletedEntity_count": "0",
|
||||||
|
"max_orderId_count": "0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 401 | Missing or invalid POS Authorization token |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
1
quixpos2jtl
Submodule
1
quixpos2jtl
Submodule
Submodule quixpos2jtl added at a0a2e8d87f
@@ -55,6 +55,9 @@ export const loggerMiddleware = (req, res, next) => {
|
|||||||
if (req.headers['content-type']) {
|
if (req.headers['content-type']) {
|
||||||
console.log(chalk.gray(' content-type:'), chalk.cyan(req.headers['content-type']));
|
console.log(chalk.gray(' content-type:'), chalk.cyan(req.headers['content-type']));
|
||||||
}
|
}
|
||||||
|
if (req.headers.authorization) {
|
||||||
|
console.log(chalk.gray(' authorization:'), chalk.cyan(req.headers.authorization));
|
||||||
|
}
|
||||||
logField('body', req.body ?? {}, { skipEmpty: true });
|
logField('body', req.body ?? {}, { skipEmpty: true });
|
||||||
logField('response', getBody());
|
logField('response', getBody());
|
||||||
|
|
||||||
|
|||||||
@@ -1997,7 +1997,7 @@ export function register(app, basePath = '') {
|
|||||||
* /article/{sku}/articlelist:
|
* /article/{sku}/articlelist:
|
||||||
* put:
|
* put:
|
||||||
* summary: Set Stückliste (bundle items) for an article
|
* summary: Set Stückliste (bundle items) for an article
|
||||||
* description: Configures the article as a Stückliste covering a list of count * article. An article that is a father or has options cannot be a Stückliste. Pass an empty array to remove the Stückliste.
|
* description: Configures the article as a Stückliste covering a list of count * article. A father article cannot be a Stückliste. Can be combined with options. Pass an empty array to remove the Stückliste.
|
||||||
* tags:
|
* tags:
|
||||||
* - Article
|
* - Article
|
||||||
* security:
|
* security:
|
||||||
@@ -2135,7 +2135,7 @@ export function register(app, basePath = '') {
|
|||||||
* /article/{sku}/optionslist:
|
* /article/{sku}/optionslist:
|
||||||
* put:
|
* put:
|
||||||
* summary: Set options list for an article
|
* summary: Set options list for an article
|
||||||
* description: Configures options for an article. Some options can be mandated (come with the article in any case). An article that is a father or a Stückliste cannot have options. Pass an empty array to remove options.
|
* description: Configures options for an article. Some options can be mandated (come with the article in any case). A father article cannot have options. Can be combined with a Stückliste. Pass an empty array to remove options.
|
||||||
* tags:
|
* tags:
|
||||||
* - Article
|
* - Article
|
||||||
* security:
|
* security:
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export function register(app, basePath = '') {
|
|||||||
* get:
|
* get:
|
||||||
* tags: [Category]
|
* tags: [Category]
|
||||||
* summary: Articles in category
|
* summary: Articles in category
|
||||||
* description: Returns articles in a category the user is authorized for.
|
* description: Returns articles in a category the user is authorized for (omits child articles of a father article). Each article includes `childCount` (the number of its child articles).
|
||||||
* security:
|
* security:
|
||||||
* - cookieAuth: []
|
* - cookieAuth: []
|
||||||
* x-markdown: category/articles.md
|
* x-markdown: category/articles.md
|
||||||
@@ -315,6 +315,46 @@ export function register(app, basePath = '') {
|
|||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* type: object
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* sku:
|
||||||
|
* type: string
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* slug:
|
||||||
|
* type: string
|
||||||
|
* net:
|
||||||
|
* type: number
|
||||||
|
* nullable: true
|
||||||
|
* taxRate:
|
||||||
|
* type: number
|
||||||
|
* nullable: true
|
||||||
|
* track_stock:
|
||||||
|
* type: boolean
|
||||||
|
* optionsCount:
|
||||||
|
* type: integer
|
||||||
|
* articleListCount:
|
||||||
|
* type: integer
|
||||||
|
* childCount:
|
||||||
|
* type: integer
|
||||||
|
* description: Number of child articles belonging to this article
|
||||||
|
* fullHash:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* thumbW:
|
||||||
|
* type: integer
|
||||||
|
* nullable: true
|
||||||
|
* thumbH:
|
||||||
|
* type: integer
|
||||||
|
* nullable: true
|
||||||
|
* thumbHash:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* brand:
|
||||||
|
* type: object
|
||||||
|
* nullable: true
|
||||||
|
* additionalProperties: true
|
||||||
* additionalProperties: true
|
* additionalProperties: true
|
||||||
* 400:
|
* 400:
|
||||||
* description: Invalid id
|
* description: Invalid id
|
||||||
@@ -342,7 +382,13 @@ export function register(app, basePath = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const articles = await db.getCategoryArticles(req.user.id, parsed.id);
|
const allArticles = await db.getCategoryArticles(req.user.id, parsed.id);
|
||||||
|
const articles = allArticles
|
||||||
|
.filter((a) => a.fatherArticleId == null)
|
||||||
|
.map((a) => ({
|
||||||
|
...a,
|
||||||
|
childCount: Number(a.childCount ?? 0),
|
||||||
|
}));
|
||||||
res.status(200).json({ articles });
|
res.status(200).json({ articles });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const status = categoryErrorStatus(error);
|
const status = categoryErrorStatus(error);
|
||||||
|
|||||||
@@ -40,7 +40,48 @@ export async function findByPairingpin(pairingpin) {
|
|||||||
return store.posterminal.findByPairingpin(pairingpin);
|
return store.posterminal.findByPairingpin(pairingpin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function findByToken(token) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.posterminal.findByToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
export async function pair(id, data) {
|
export async function pair(id, data) {
|
||||||
const store = await getStore();
|
const store = await getStore();
|
||||||
return store.posterminal.pair(id, data);
|
return store.posterminal.pair(id, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getProductCount(params) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.posterminal.getProductCount(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCategoryCount(params) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.posterminal.getCategoryCount(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCustomerCount(params) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.posterminal.getCustomerCount(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCustomerGroupCount(params) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.posterminal.getCustomerGroupCount(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCompositeProductCount(params) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.posterminal.getCompositeProductCount(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDeletedEntityCount(params) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.posterminal.getDeletedEntityCount(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMaxOrderIdCount(params) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.posterminal.getMaxOrderIdCount(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import { posAuthMiddleware } from './middleware/posAuth.js';
|
||||||
import { handleClient } from './routes/client.js';
|
import { handleClient } from './routes/client.js';
|
||||||
|
import { handleInit } from './routes/init.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// POS discovery and pairing
|
// POS discovery and pairing
|
||||||
router.get('/client', handleClient);
|
router.get('/client', handleClient);
|
||||||
|
|
||||||
|
// POS sync initialization counts
|
||||||
|
router.get('/init', posAuthMiddleware, handleInit);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|||||||
66
src/v1/middleware/posAuth.js
Normal file
66
src/v1/middleware/posAuth.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import * as db from '../../db/posterminal.js';
|
||||||
|
|
||||||
|
export function extractPosToken(req) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (typeof authHeader === 'string' && authHeader.trim()) {
|
||||||
|
const trimmed = authHeader.trim();
|
||||||
|
if (/^Bearer\s+/i.test(trimmed)) {
|
||||||
|
return trimmed.replace(/^Bearer\s+/i, '').trim();
|
||||||
|
}
|
||||||
|
if (/^Basic\s+/i.test(trimmed)) {
|
||||||
|
const b64 = trimmed.replace(/^Basic\s+/i, '').trim();
|
||||||
|
try {
|
||||||
|
const decoded = Buffer.from(b64, 'base64').toString('utf8');
|
||||||
|
const parts = decoded.split(':');
|
||||||
|
return (parts[1] || parts[0] || '').trim();
|
||||||
|
} catch {
|
||||||
|
return b64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of ['x-auth-token', 'authtoken', 'auth-token', 'token', 'x-token']) {
|
||||||
|
const val = req.headers[key];
|
||||||
|
if (typeof val === 'string' && val.trim()) {
|
||||||
|
return val.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryToken = req.query?.authToken || req.query?.token;
|
||||||
|
if (typeof queryToken === 'string' && queryToken.trim()) {
|
||||||
|
return queryToken.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the POS terminal token from the Authorization header and attaches
|
||||||
|
* `req.posterminal`.
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
* @param {import('express').NextFunction} next
|
||||||
|
*/
|
||||||
|
export async function posAuthMiddleware(req, res, next) {
|
||||||
|
const token = extractPosToken(req);
|
||||||
|
if (!token) {
|
||||||
|
console.log(chalk.yellow(' [posAuth] No token found in headers:'), req.headers);
|
||||||
|
return res.status(401).json({ Message: 'Authorization has been denied for this request.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const terminal = await db.findByToken(token);
|
||||||
|
if (!terminal) {
|
||||||
|
console.log(chalk.yellow(` [posAuth] Unknown token "${token}", headers:`), req.headers);
|
||||||
|
return res.status(401).json({ Message: 'Authorization has been denied for this request.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.posterminal = terminal;
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('POS auth error:', err);
|
||||||
|
return res.status(500).json({ Message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
|
|||||||
import * as db from '../../db/posterminal.js';
|
import * as db from '../../db/posterminal.js';
|
||||||
import { getCompanyById } from '../../db/company.js';
|
import { getCompanyById } from '../../db/company.js';
|
||||||
import { getCertMetadata } from '../../util/cert.js';
|
import { getCertMetadata } from '../../util/cert.js';
|
||||||
|
import { emit } from '../../api/sse.js';
|
||||||
|
|
||||||
export function serverTimestamp() {
|
export function serverTimestamp() {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
@@ -166,6 +167,10 @@ export async function handleClient(req, res) {
|
|||||||
pairedAt,
|
pairedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (terminal.companyId) {
|
||||||
|
emit('posterminal', { companyId: terminal.companyId });
|
||||||
|
}
|
||||||
|
|
||||||
let mandantName = process.env.MANDANT_NAME || 'eB-Standard';
|
let mandantName = process.env.MANDANT_NAME || 'eB-Standard';
|
||||||
let mandantDatabase = process.env.MANDANT_DATABASE || 'eazybusiness';
|
let mandantDatabase = process.env.MANDANT_DATABASE || 'eazybusiness';
|
||||||
let mandantId = String(terminal.companyId || process.env.MANDANT_ID || '1');
|
let mandantId = String(terminal.companyId || process.env.MANDANT_ID || '1');
|
||||||
|
|||||||
165
src/v1/routes/init.js
Normal file
165
src/v1/routes/init.js
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import * as db from '../../db/posterminal.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/init:
|
||||||
|
* get:
|
||||||
|
* tags: [POS]
|
||||||
|
* summary: POS sync initialization counts
|
||||||
|
* description: |
|
||||||
|
* Returns entity change counts since the provided `lastChanged*` unix timestamp cursors
|
||||||
|
* for the authenticated POS terminal (`Authorization: Bearer <authToken>`).
|
||||||
|
* x-markdown: pos/init.md
|
||||||
|
* parameters:
|
||||||
|
* - in: header
|
||||||
|
* name: Authorization
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: POS device authToken obtained during pairing (`Bearer <token>` or `<token>`)
|
||||||
|
* example: "Bearer c30099b66ec1fcb9ccd05795423c94e8"
|
||||||
|
* - in: query
|
||||||
|
* name: lastChangedProduct
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 0
|
||||||
|
* description: Unix timestamp of last synced product update
|
||||||
|
* example: 0
|
||||||
|
* - in: query
|
||||||
|
* name: lastChangedCategory
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 0
|
||||||
|
* description: Unix timestamp of last synced category update
|
||||||
|
* example: 0
|
||||||
|
* - in: query
|
||||||
|
* name: lastChangedCustomer
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 0
|
||||||
|
* description: Unix timestamp of last synced customer update
|
||||||
|
* example: 0
|
||||||
|
* - in: query
|
||||||
|
* name: lastChangedCustomerGroup
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 0
|
||||||
|
* description: Unix timestamp of last synced customer group update
|
||||||
|
* example: 0
|
||||||
|
* - in: query
|
||||||
|
* name: lastChangedCompositeProduct
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 0
|
||||||
|
* description: Unix timestamp of last synced composite product update
|
||||||
|
* example: 0
|
||||||
|
* - in: query
|
||||||
|
* name: lastChangedDeletedEntity
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 0
|
||||||
|
* description: Unix timestamp of last synced deleted entity
|
||||||
|
* example: 0
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Sync initialization counts
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* version:
|
||||||
|
* type: string
|
||||||
|
* example: "1.10.12.0"
|
||||||
|
* product_count:
|
||||||
|
* type: string
|
||||||
|
* example: "19"
|
||||||
|
* category_count:
|
||||||
|
* type: string
|
||||||
|
* example: "4"
|
||||||
|
* customer_count:
|
||||||
|
* type: string
|
||||||
|
* example: "0"
|
||||||
|
* customerGroup_count:
|
||||||
|
* type: string
|
||||||
|
* example: "1"
|
||||||
|
* compositeProduct_count:
|
||||||
|
* type: string
|
||||||
|
* example: "0"
|
||||||
|
* configurationGroup_count:
|
||||||
|
* type: string
|
||||||
|
* example: "0"
|
||||||
|
* configurationItem_count:
|
||||||
|
* type: string
|
||||||
|
* example: "0"
|
||||||
|
* deletedEntity_count:
|
||||||
|
* type: string
|
||||||
|
* example: "0"
|
||||||
|
* max_orderId_count:
|
||||||
|
* type: string
|
||||||
|
* example: "0"
|
||||||
|
* 401:
|
||||||
|
* description: Missing or invalid POS Authorization token
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* Message:
|
||||||
|
* type: string
|
||||||
|
* example: "Authorization has been denied for this request."
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
export async function handleInit(req, res) {
|
||||||
|
const posterminalId = req.posterminal.id;
|
||||||
|
|
||||||
|
const productCursor = Number(req.query.lastChangedProduct) || 0;
|
||||||
|
const categoryCursor = Number(req.query.lastChangedCategory) || 0;
|
||||||
|
const customerCursor = Number(req.query.lastChangedCustomer) || 0;
|
||||||
|
const customerGroupCursor = Number(req.query.lastChangedCustomerGroup) || 0;
|
||||||
|
const compositeProductCursor = Number(req.query.lastChangedCompositeProduct) || 0;
|
||||||
|
const deletedEntityCursor = Number(req.query.lastChangedDeletedEntity) || 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [
|
||||||
|
productCount,
|
||||||
|
categoryCount,
|
||||||
|
customerCount,
|
||||||
|
customerGroupCount,
|
||||||
|
compositeProductCount,
|
||||||
|
deletedEntityCount,
|
||||||
|
maxOrderIdCount,
|
||||||
|
] = await Promise.all([
|
||||||
|
db.getProductCount({ posterminalId, cursor: productCursor }),
|
||||||
|
db.getCategoryCount({ posterminalId, cursor: categoryCursor }),
|
||||||
|
db.getCustomerCount({ posterminalId, cursor: customerCursor }),
|
||||||
|
db.getCustomerGroupCount({ posterminalId, cursor: customerGroupCursor }),
|
||||||
|
db.getCompositeProductCount({ posterminalId, cursor: compositeProductCursor }),
|
||||||
|
db.getDeletedEntityCount({ posterminalId, cursor: deletedEntityCursor }),
|
||||||
|
db.getMaxOrderIdCount({ posterminalId }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
version: '1.10.12.0',
|
||||||
|
product_count: String(productCount),
|
||||||
|
category_count: String(categoryCount),
|
||||||
|
customer_count: String(customerCount),
|
||||||
|
customerGroup_count: String(customerGroupCount),
|
||||||
|
compositeProduct_count: String(compositeProductCount),
|
||||||
|
configurationGroup_count: '0',
|
||||||
|
configurationItem_count: '0',
|
||||||
|
deletedEntity_count: String(deletedEntityCount),
|
||||||
|
max_orderId_count: String(maxOrderIdCount),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('POS /v1/init error:', err);
|
||||||
|
return res.status(500).json({ Message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user